[
  {
    "path": ".all-contributorsrc",
    "content": "{\n  \"files\": [\n    \"README.md\"\n  ],\n  \"imageSize\": 100,\n  \"commit\": false,\n  \"contributors\": [\n    {\n      \"login\": \"shilangyu\",\n      \"name\": \"Marcin Wojnarowski\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/29288116?v=4\",\n      \"profile\": \"http://shilangyu.github.io\",\n      \"contributions\": [\n        \"code\",\n        \"platform\"\n      ]\n    },\n    {\n      \"login\": \"mqudsi\",\n      \"name\": \"Mahmoud Al-Qudsi\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/606923?v=4\",\n      \"profile\": \"http://neosmart.net/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"andys8\",\n      \"name\": \"Andy\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/13085980?v=4\",\n      \"profile\": \"https://andys8.de\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"HarHarLinks\",\n      \"name\": \"Kim Brose\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/2803622?v=4\",\n      \"profile\": \"https://github.com/HarHarLinks\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"svenstaro\",\n      \"name\": \"Sven-Hendrik Haase\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/1664?v=4\",\n      \"profile\": \"https://svenstaro.org\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"tim77\",\n      \"name\": \"Artem Polishchuk\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/5614476?v=4\",\n      \"profile\": \"https://liberapay.com/Artem4/\",\n      \"contributions\": [\n        \"platform\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"runlevel5\",\n      \"name\": \"Trung Lê\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/135605?v=4\",\n      \"profile\": \"http://ruby-journal.com/\",\n      \"contributions\": [\n        \"platform\",\n        \"infra\"\n      ]\n    },\n    {\n      \"login\": \"dm9pZCAq\",\n      \"name\": \"dm9pZCAq\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/46228973?v=4\",\n      \"profile\": \"https://github.com/dm9pZCAq\",\n      \"contributions\": [\n        \"platform\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"LlinksRechts\",\n      \"name\": \"Lukas Rysavy\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/10536802?v=4\",\n      \"profile\": \"https://lukor.org\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"ehamberg\",\n      \"name\": \"Erlend Hamberg\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/16063?v=4\",\n      \"profile\": \"http://hamberg.no/erlend\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Frederick888\",\n      \"name\": \"Frederick Zhang\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/4507647?v=4\",\n      \"profile\": \"https://onee3.org\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"pvanheus\",\n      \"name\": \"pvanheus\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/4154788?v=4\",\n      \"profile\": \"https://github.com/pvanheus\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"briandipalma\",\n      \"name\": \"Brian Di Palma\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1597820?v=4\",\n      \"profile\": \"https://github.com/briandipalma\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"dakyskye\",\n      \"name\": \"Lasha Kanteladze\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/32128756?v=4\",\n      \"profile\": \"https://dakyskye.github.io\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"herbygillot\",\n      \"name\": \"Herby Gillot\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/618376?v=4\",\n      \"profile\": \"https://github.com/herbygillot\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"yellowsquid\",\n      \"name\": \"Greg Brown\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/46519298?v=4\",\n      \"profile\": \"https://github.com/yellowsquid\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"TotalCaesar659\",\n      \"name\": \"TotalCaesar659\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/14265316?v=4\",\n      \"profile\": \"https://github.com/TotalCaesar659\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"grawlinson\",\n      \"name\": \"George Rawlinson\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/4408051?v=4\",\n      \"profile\": \"https://github.com/grawlinson\",\n      \"contributions\": [\n        \"doc\",\n        \"platform\"\n      ]\n    },\n    {\n      \"login\": \"adiabatic\",\n      \"name\": \"adiabatic\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/101246?v=4\",\n      \"profile\": \"https://www.frogorbits.com/\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"bowlofeggs\",\n      \"name\": \"Randy Barlow\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/354506?v=4\",\n      \"profile\": \"https://electronsweatshop.com\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"patricksjackson\",\n      \"name\": \"Patrick Jackson\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/160646?v=4\",\n      \"profile\": \"http://jackson.dev\",\n      \"contributions\": [\n        \"ideas\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"mati865\",\n      \"name\": \"Mateusz Mikuła\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1174646?v=4\",\n      \"profile\": \"https://github.com/mati865\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"GuillaumeGomez\",\n      \"name\": \"Guillaume Gomez\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/3050060?v=4\",\n      \"profile\": \"https://blog.guillaume-gomez.fr\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"shurizzle\",\n      \"name\": \"shura\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/203655?v=4\",\n      \"profile\": \"https://github.com/shurizzle\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"wezm\",\n      \"name\": \"Wesley Moore\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/21787?v=4\",\n      \"profile\": \"https://www.wezm.net/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"xgdgsc\",\n      \"name\": \"xgdgsc\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1189869?v=4\",\n      \"profile\": \"https://github.com/xgdgsc\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"ViridiCanis\",\n      \"name\": \"ViridiCanis\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/49595344?v=4\",\n      \"profile\": \"https://github.com/ViridiCanis\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"jamartin9\",\n      \"name\": \"Justin Martin\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/7027701?v=4\",\n      \"profile\": \"https://github.com/jamartin9\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"DianaNites\",\n      \"name\": \"Diana\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/5275194?v=4\",\n      \"profile\": \"https://github.com/DianaNites\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"hervyqa\",\n      \"name\": \"Hervy Qurrotul Ainur Rozi\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/45872139?v=4\",\n      \"profile\": \"https://hervyqa.id\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"mrivnak\",\n      \"name\": \"Mike Rivnak\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/7389355?v=4\",\n      \"profile\": \"https://mrivnak.github.io\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"lroobrou\",\n      \"name\": \"lroobrou\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/35152113?v=4\",\n      \"profile\": \"https://github.com/lroobrou\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"database64128\",\n      \"name\": \"database64128\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/18757988?v=4\",\n      \"profile\": \"https://cube64128.xyz/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"sou-chon\",\n      \"name\": \"Chon Sou\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/35537528?v=4\",\n      \"profile\": \"https://github.com/sou-chon\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Drsheppard01\",\n      \"name\": \"DrSheppard\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/60893791?v=4\",\n      \"profile\": \"https://github.com/Drsheppard01\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"RaresCon\",\n      \"name\": \"Rareș Constantin\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/95525840?v=4\",\n      \"profile\": \"https://github.com/RaresCon\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"felipesuri\",\n      \"name\": \"felipesuri\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/50281523?v=4\",\n      \"profile\": \"http://felipesuri.com\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"spital\",\n      \"name\": \"spital\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/11034264?v=4\",\n      \"profile\": \"https://github.com/spital\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"mbikovitsky\",\n      \"name\": \"Michael Bikovitsky\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1389811?v=4\",\n      \"profile\": \"https://bikodbg.com/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"dvalter\",\n      \"name\": \"Dmitry Valter\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/38795282?v=4\",\n      \"profile\": \"https://github.com/dvalter\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"aragonnetje6\",\n      \"name\": \"Grace Stok\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/69118097?v=4\",\n      \"profile\": \"https://github.com/aragonnetje6\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"yshui\",\n      \"name\": \"Yuxuan Shui\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/366851?v=4\",\n      \"profile\": \"https://github.com/yshui\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"WenqingZong\",\n      \"name\": \"Wenqing Zong\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/43934749?v=4\",\n      \"profile\": \"http://zongwenqing.com\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"gabelluardo\",\n      \"name\": \"Gabriele Belluardo\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/42920247?v=4\",\n      \"profile\": \"http://gabelluardo.github.io\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"zebp\",\n      \"name\": \"Zeb Piasecki\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/14242997?v=4\",\n      \"profile\": \"https://zebulon.dev/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Freed-Wu\",\n      \"name\": \"wzy\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/32936898?v=4\",\n      \"profile\": \"https://freed-wu.github.io/\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"john-s-lin\",\n      \"name\": \"john-s-lin\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/66440371?v=4\",\n      \"profile\": \"https://johnlin.ca/\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"lyuha\",\n      \"name\": \"Lee Wonjoon\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/4014016?v=4\",\n      \"profile\": \"https://github.com/lyuha\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"davlgd\",\n      \"name\": \"David Legrand\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1110600?v=4\",\n      \"profile\": \"https://www.davlgd.fr\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"MichalBryxi\",\n      \"name\": \"Michal Bryxí\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/847473?v=4\",\n      \"profile\": \"https://github.com/MichalBryxi\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"TheSkyentist\",\n      \"name\": \"Raphael Erik Hviding\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/17031860?v=4\",\n      \"profile\": \"http://mpia.de/~hviding/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"CosmicHorrorDev\",\n      \"name\": \"CosmicHorror\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/30302768?v=4\",\n      \"profile\": \"http://cosmichorror.dev\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"woodsb02\",\n      \"name\": \"Ben Woods\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/7113557?v=4\",\n      \"profile\": \"https://www.woods.am/\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"stephen-huan\",\n      \"name\": \"Stephen Huan\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/20411956?v=4\",\n      \"profile\": \"http://cgdct.moe\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"jasongwartz\",\n      \"name\": \"Jason Gwartz\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/10981911?v=4\",\n      \"profile\": \"https://github.com/jasongwartz\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"llc0930\",\n      \"name\": \"llc0930\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/14966910?v=4\",\n      \"profile\": \"https://github.com/llc0930\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"yretenai\",\n      \"name\": \"Ada Ahmed\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/614231?v=4\",\n      \"profile\": \"https://chronovore.dev\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Wateir\",\n      \"name\": \"Wateir\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/78731687?v=4\",\n      \"profile\": \"https://github.com/Wateir\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"al42and\",\n      \"name\": \"Andrey Alekseenko\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/933873?v=4\",\n      \"profile\": \"https://github.com/al42and\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"fgimian\",\n      \"name\": \"Fotis Gimian\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1811813?v=4\",\n      \"profile\": \"http://fgimian.github.io/\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"SigmaSquadron\",\n      \"name\": \"Fernando Rodrigues\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/174749595?v=4\",\n      \"profile\": \"https://sigmasquadron.net\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"mtoohey31\",\n      \"name\": \"Matthew Toohey\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/36740602?v=4\",\n      \"profile\": \"https://mtoohey.com\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"win8linux\",\n      \"name\": \"Julius Enriquez\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/11584387?v=4\",\n      \"profile\": \"https://meander.site\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"benjamb\",\n      \"name\": \"Ben Brown\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/8291297?v=4\",\n      \"profile\": \"https://github.com/benjamb\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"nyurik\",\n      \"name\": \"Yuri Astrakhan\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1641515?v=4\",\n      \"profile\": \"https://github.com/nyurik\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"kachick\",\n      \"name\": \"Kenichi Kamiya\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/1180335?v=4\",\n      \"profile\": \"https://kachick.github.io/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"yahlia\",\n      \"name\": \"yahlia\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/40295453?v=4\",\n      \"profile\": \"https://github.com/yahlia\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Bucket-Bucket-Bucket\",\n      \"name\": \"Bucket-Bucket-Bucket\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/107044719?v=4\",\n      \"profile\": \"https://github.com/Bucket-Bucket-Bucket\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"marverix\",\n      \"name\": \"Marek Sierociński\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/2142811?v=4\",\n      \"profile\": \"http://marek.sierocinscy.pl\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"Tommimon\",\n      \"name\": \"Tommaso Montanari\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/37435103?v=4\",\n      \"profile\": \"https://github.com/Tommimon\",\n      \"contributions\": [\n        \"design\",\n        \"ideas\"\n      ]\n    },\n    {\n      \"login\": \"jylenhof\",\n      \"name\": \"Jean-Yves LENHOF\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/36410287?v=4\",\n      \"profile\": \"http://blog.lenhof.eu.org\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"Saphereye\",\n      \"name\": \"Adarsh Das\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/59739923?v=4\",\n      \"profile\": \"http://saphereye.github.io\",\n      \"contributions\": [\n        \"code\",\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"oxyzenQ\",\n      \"name\": \"rezky_nightky\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/130107241?v=4\",\n      \"profile\": \"https://github.com/oxyzenQ\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"gitgoggles\",\n      \"name\": \"gitgoggles\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/101480183?v=4\",\n      \"profile\": \"https://github.com/gitgoggles\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"thunze\",\n      \"name\": \"Tom\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/22795263?v=4\",\n      \"profile\": \"https://github.com/thunze\",\n      \"contributions\": [\n        \"maintenance\"\n      ]\n    },\n    {\n      \"login\": \"ggaddy\",\n      \"name\": \"G\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/13815367?v=4\",\n      \"profile\": \"https://github.com/ggaddy\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"lpnh\",\n      \"name\": \"Filipe Paniguel\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/90577992?v=4\",\n      \"profile\": \"https://github.com/lpnh\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"WqyJh\",\n      \"name\": \"Qiying Wang\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/15232241?v=4\",\n      \"profile\": \"https://www.jianshu.com/u/f5754cd2e83d\",\n      \"contributions\": [\n        \"code\"\n      ]\n    }\n  ],\n  \"contributorsPerLine\": 7,\n  \"projectName\": \"bottom\",\n  \"projectOwner\": \"ClementTsang\",\n  \"repoType\": \"github\",\n  \"repoHost\": \"https://github.com\",\n  \"skipCi\": false,\n  \"commitConvention\": \"angular\",\n  \"commitType\": \"docs\"\n}\n"
  },
  {
    "path": ".cargo/config.toml",
    "content": "[target.x86_64-pc-windows-msvc]\nrustflags = [\"-C\", \"target-feature=+crt-static\"]\n\n[target.i686-pc-windows-msvc]\nrustflags = [\"-C\", \"target-feature=+crt-static\"]\n"
  },
  {
    "path": ".cirrus.yml",
    "content": "%YAML 1.1\n---\n# Configuration for CirrusCI. This is primarily used for testing and building FreeBSD and old versions of Linux,\n# since other CI platforms don't support build jobs for these configurations.\n#\n# Note that we set the YAML directive above to prevent some linting errors around the templates.\n\nsetup_template: &SETUP_TEMPLATE\n  setup_script:\n    - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs --output rustup.sh\n    - sh rustup.sh --default-toolchain stable -y\n\ncache_template: &CACHE_TEMPLATE\n  registry_cache:\n    folder: $HOME/.cargo/registry\n    reupload_on_changes: \"true\"\n    fingerprint_script:\n      - $HOME/.cargo/bin/rustc --version\n      - cat Cargo.lock\n      - echo $CIRRUS_OS\n      - echo $CIRRUS_TASK_NAME\n  target_cache:\n    folder: target\n    reupload_on_changes: \"true\"\n    fingerprint_script:\n      - $HOME/.cargo/bin/rustc --version\n      - cat Cargo.lock\n      - echo $CIRRUS_OS\n      - echo $CIRRUS_TASK_NAME\n\ncleanup_template: &CLEANUP_TEMPLATE\n  before_cache_script:\n    - rm -rf $HOME/.cargo/registry/index\n    - rm -rf $HOME/.cargo/registry/src\n    - rm -f ./target/.rustc_info.json\n\nenv:\n  CARGO_INCREMENTAL: \"0\"\n  CARGO_PROFILE_DEV_DEBUG: \"0\"\n  CARGO_HUSKY_DONT_INSTALL_HOOKS: \"true\"\n\nrelease_task:\n  auto_cancellation: \"false\"\n  only_if: $CIRRUS_BUILD_SOURCE == \"api\" && $BTM_BUILD_RELEASE_CALLER == \"ci\"\n  timeout_in: \"30m\"\n  env:\n    BTM_GENERATE: \"true\"\n    COMPLETION_DIR: \"target/tmp/bottom/completion/\"\n    MANPAGE_DIR: \"target/tmp/bottom/manpage/\"\n    # -PLACEHOLDER FOR CI-\n  matrix:\n    - name: \"Legacy Linux (2.17)\"\n      alias: \"linux_2_17_build\"\n      container:\n        image: quay.io/pypa/manylinux2014_x86_64\n      env:\n        TARGET: \"x86_64-unknown-linux-gnu\"\n        NAME: \"x86_64-unknown-linux-gnu-2-17\"\n  <<: *SETUP_TEMPLATE\n  <<: *CACHE_TEMPLATE\n  build_script:\n    - . $HOME/.cargo/env\n    - cargo build --release --locked --features deploy\n    - mv ./target/release/btm ./\n    - ./btm -V\n    - mv \"$COMPLETION_DIR\" completion\n    - mv \"$MANPAGE_DIR\" manpage\n    - tar -czvf bottom_$NAME.tar.gz btm completion\n  binaries_artifacts:\n    path: bottom_$NAME.tar.gz\n  <<: *CLEANUP_TEMPLATE\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug report\ndescription: Found something wrong or broken? If it hasn't already been filed/solved, report it!\nlabels: [\"bug\"]\nbody:\n  - type: checkboxes\n    id: acknowledgements\n    attributes:\n      label: Checklist\n      options:\n        - label: >\n            I've looked through the [troubleshooting docs](https://bottom.pages.dev/nightly/troubleshooting),\n            [the known problems list](https://bottom.pages.dev/nightly/support/official/#known-problems), and \n            [existing open issues](https://github.com/ClementTsang/bottom/issues?q=is%3Aopen+is%3Aissue) for similar\n            issues.\n          required: true\n\n  - type: input\n    id: operating_system\n    attributes:\n      label: What operating system and version are you using?\n      description: >\n        Please provide the operating system(s) and version(s) that are experiencing the problem.\n        Note that issues on operating systems that [are not officially supported](https://github.com/ClementTsang/bottom#support)\n        may not be prioritized/resolved.\n      placeholder: Arch Linux 6.6.2\n\n  - type: dropdown\n    id: architecture\n    attributes:\n      label: What architecture are you using?\n      description: >\n        Please select the architecture(s) that are experiencing the problem.\n        Note that systems that [are not officially supported](https://github.com/ClementTsang/bottom#support)\n        may not be prioritized/resolved.\n      multiple: true\n      options:\n        - x86_64/AMD64\n        - arm64\n        - x86\n        - arm32\n        - Other (please specify in the Additional Information area at the end)\n\n  - type: textarea\n    id: terminal\n    attributes:\n      label: What terminal(s) are you running bottom on that are experiencing the problem?\n      description: >\n        Please provide what terminal(s) you are running `bottom` on (e.g. Konsole, kitty, urxvt)\n        that are experiencing the issue, as well as their version and any relevant settings (e.g. terminal theme).\n      placeholder: kitty 0.25.2\n\n  - type: dropdown\n    id: filesystem\n    validations:\n      required: false\n    attributes:\n      label: (Optional) What filesystem(s) are you using?\n      description: >\n        If you know, please select what filesystem(s) you are using on the system that is experiencing the problem. This\n        can be especially helpful if the issue is related to either the disk or memory widgets.\n      multiple: true\n      options:\n        - ext4\n        - NTFS\n        - exFAT\n        - FAT\n        - ZFS\n        - Btrfs\n        - APFS\n        - Other (please specify in the Additional Information area at the end)\"\n\n  - type: input\n    id: version\n    validations:\n      required: true\n    attributes:\n      label: What version of bottom are you running?\n      description: >\n        Please specify which version of `bottom` you're running that is causing problems. You can find this with\n        `btm -V`. If you are using a nightly/non-release version, please also specify that.\n\n        It would also be helpful if you are not running [the latest version](https://github.com/ClementTsang/bottom/releases/latest)\n        to try that as well to see if the issue has already been resolved.\n      placeholder: 0.12.3\n\n  - type: textarea\n    id: install\n    validations:\n      required: true\n    attributes:\n      label: How did you install bottom?\n      description: >\n        Please describe how you installed `bottom`. If you manually compiled it, please also mention your _Rust version_.\n\n        **Note: if you installed `bottom` from cargo, please ensure that you installed the right crate (https://crates.io/crates/bottom).**\n      placeholder: Installed bottom through the Arch official repos.\n\n  # TODO: After some point also add in a `btm check` invocation\n\n  - type: textarea\n    id: description\n    validations:\n      required: true\n    attributes:\n      label: Describe the issue\n      description: >\n        Give a description of the issue. If possible, provide screenshots/videos.\n      placeholder: |\n        Example:\n        bottom is failing to output information for a mounted encrypted partition on basic mode. It should be able to report this information, but I'm not seeing the entry at all.\n\n  - type: textarea\n    id: expected\n    validations:\n      required: true\n    attributes:\n      label: What is the expected behaviour?\n      description: >\n        Describe the behaviour you expected.\n      placeholder: |\n        Example:\n        I expect to be able to see information about the encrypted partition on basic mode.\n\n  - type: textarea\n    id: actual\n    validations:\n      required: true\n    attributes:\n      label: What is the actual behaviour?\n      description: >\n        Describe the behaviour you actually see. If possible, provide screenshots/videos.\n      placeholder: |\n        Example:\n        I am unable to see information about my encrypted partition.\n\n  - type: textarea\n    id: reproduce\n    validations:\n      required: true\n    attributes:\n      label: How can we reproduce this?\n      description: >\n        Provide detailed steps on _how_ to reproduce your problem, to the best of your ability. Be as detailed as\n        possible. Include any config files or flags used. If possible, provide screenshots/videos of the issue.\n\n        Remember - if maintainers cannot reproduce the issue, it will be very hard to fix!\n      placeholder: |\n        Example:\n          1. Mount a LUKS encrypted partition.\n          2. Run `btm --basic`\n          3. Observe there is no partition shown.\n\n  - type: textarea\n    id: additional\n    attributes:\n      label: Additional information\n      description: Provide any additional information you think may be relevant or helpful.\n      placeholder: It works fine if I just run it normally without the `--basic` flag.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Open a discussion\n    about: |\n      Got a question about using bottom? Need help troubleshooting something? Or maybe just want to talk about something related to bottom? Feel free to open a discussion!\n    url: https://github.com/ClementTsang/bottom/discussions/new\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature request\ndescription: Got a good idea that hasn't already been suggested?  Mention it here!\nlabels: [\"feature\"]\nbody:\n  - type: checkboxes\n    id: acknowledgements\n    attributes:\n      label: Checklist\n      options:\n        - label: >\n            I've looked through [the documentation](https://bottom.pages.dev/nightly/) and \n            [existing open issues](https://github.com/ClementTsang/bottom/issues?q=is%3Aopen+is%3Aissue+label%3Afeature)\n            for similar feature requests.\n          required: true\n\n  - type: textarea\n    id: description\n    validations:\n      required: true\n    attributes:\n      label: Describe the feature request\n      description: >\n        Please describe what behaviour you are looking for, the motivation for it, and use cases where this feature\n        would be helpful to both you and others. Try to be clear and concise.\n\n        If you have any ideas to implement this feature as well, feel free to write them down here too.\n      placeholder: |\n        Example:\n        It would be nice to support FreeBSD, as I and others often use similar tools on my FreeBSD-based system.\n        I also noticed that sysinfo has FreeBSD support as a data source.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/packaging.yml",
    "content": "name: Packaging\ndescription: For issues, questions, or requests regarding packaging or distribution.\nlabels: [\"packaging\"]\nbody:\n  - type: markdown\n    attributes:\n      value: >\n        If this is an issue about supporting a new package/installation method for a platform you use, please\n        consider maintaining it yourself/with others and submitting a PR or issue with a link to it - they'll be\n        very much appreciated and likely added to the README quickly. [The documentation on packaging/distribution](https://bottom.pages.dev/nightly/contribution/packaging-and-distribution/)\n        may be helpful in setting things up. If there are some issues with bottom itself causing problems with\n        packaging, feel free to open an appropriate issue.\n\n\n        If this is an issue regarding a specific existing distribution channel, feel free to report issues here if they\n        are related to the following sources:\n\n          * [crates.io](https://crates.io/crates/bottom)\n          * [Binary releases/packages released on GitHub](https://github.com/ClementTsang/bottom/releases)\n\n\n        For any other distribution channel, please first try to contact the package maintainers where appropriate\n        to get help regarding distribution-specific issues (e.g. the package has issues installing, the package\n        is outdated, etc.) before reaching out here. While I am happy to help where possible, I do not\n        personally use many of the various ways people distribute bottom. As such, I might lack the\n        platform-specific context, knowledge, or tools to be able to help you at all regarding the\n        distribution method, and the best I can do is just point you to the package maintainer.\n\n  - type: checkboxes\n    id: acknowledgements\n    attributes:\n      label: Checklist\n      options:\n        - label: >\n            I have read and understood the above text.\n          required: true\n\n  - type: textarea\n    id: description\n    validations:\n      required: true\n    attributes:\n      label: Describe the issue\n      description: >\n        What is the packaging-related issue? Please be clear and concise.\n      placeholder: |\n        Example: Would it be possible to add shell completion generation as a separate build artifact?\n"
  },
  {
    "path": ".github/actions/test-bsd-target/action.yml",
    "content": "name: Test BSD Target\ndescription: Run tests for a BSD target using VMs, with retries on failure. Needed as cross doesn't support them (https://github.com/cross-rs/cross/wiki/FAQ#running-bsd-tests).\n\ninputs:\n  target:\n    description: \"Rust target triple (e.g., x86_64-unknown-freebsd)\"\n    required: true\n  os-version:\n    description: \"OS release version (e.g., 13.2 for FreeBSD)\"\n    required: false\n\nruns:\n  using: composite\n  steps:\n    - name: FreeBSD Test (Attempt 1)\n      uses: vmactions/freebsd-vm@c9f815bc7aa0d34c9fdd0619b034a32d6ca7b57e # v1.4.2\n      if: ${{ inputs.target == 'x86_64-unknown-freebsd' }}\n      with:\n        release: \"${{ inputs.os-version }}\"\n        envs: \"RUST_BACKTRACE CARGO_INCREMENTAL CARGO_PROFILE_DEV_DEBUG CARGO_HUSKY_DONT_INSTALL_HOOKS\"\n        usesh: true\n        run: sh ./scripts/ci/bsd_tests.sh ${{ inputs.target }}\n      id: freebsd_attempt_1\n      continue-on-error: true\n\n    - name: FreeBSD Test (Attempt 2)\n      uses: vmactions/freebsd-vm@c9f815bc7aa0d34c9fdd0619b034a32d6ca7b57e # v1.4.2\n      if: ${{ inputs.target == 'x86_64-unknown-freebsd' && steps.freebsd_attempt_1.outcome == 'failure' }}\n      with:\n        release: \"${{ inputs.os-version }}\"\n        envs: \"RUST_BACKTRACE CARGO_INCREMENTAL CARGO_PROFILE_DEV_DEBUG CARGO_HUSKY_DONT_INSTALL_HOOKS\"\n        usesh: true\n        run: sh ./scripts/ci/bsd_tests.sh ${{ inputs.target }}\n      id: freebsd_attempt_2\n      continue-on-error: true\n\n    - name: FreeBSD Test (Attempt 3)\n      uses: vmactions/freebsd-vm@c9f815bc7aa0d34c9fdd0619b034a32d6ca7b57e # v1.4.2\n      if: ${{ inputs.target == 'x86_64-unknown-freebsd' && steps.freebsd_attempt_2.outcome == 'failure' }}\n      with:\n        release: \"${{ inputs.os-version }}\"\n        envs: \"RUST_BACKTRACE CARGO_INCREMENTAL CARGO_PROFILE_DEV_DEBUG CARGO_HUSKY_DONT_INSTALL_HOOKS\"\n        usesh: true\n        run: sh ./scripts/ci/bsd_tests.sh ${{ inputs.target }}\n      id: freebsd_attempt_3\n\n    - name: NetBSD Test (Attempt 1)\n      uses: vmactions/netbsd-vm@e04aec09540429f9cebb0e7941f7cd0c0fc3b44f # v1.3.6\n      if: ${{ inputs.target == 'x86_64-unknown-netbsd' }}\n      with:\n        release: \"${{ inputs.os-version }}\"\n        envs: \"RUST_BACKTRACE CARGO_INCREMENTAL CARGO_PROFILE_DEV_DEBUG CARGO_HUSKY_DONT_INSTALL_HOOKS\"\n        usesh: true\n        run: sh ./scripts/ci/bsd_tests.sh ${{ inputs.target }}\n      id: netbsd_attempt_1\n      continue-on-error: true\n\n    - name: NetBSD Test (Attempt 2)\n      uses: vmactions/netbsd-vm@e04aec09540429f9cebb0e7941f7cd0c0fc3b44f # v1.3.6\n      if: ${{ inputs.target == 'x86_64-unknown-netbsd' && steps.netbsd_attempt_1.outcome == 'failure' }}\n      with:\n        release: \"${{ inputs.os-version }}\"\n        envs: \"RUST_BACKTRACE CARGO_INCREMENTAL CARGO_PROFILE_DEV_DEBUG CARGO_HUSKY_DONT_INSTALL_HOOKS\"\n        usesh: true\n        run: sh ./scripts/ci/bsd_tests.sh ${{ inputs.target }}\n      id: netbsd_attempt_2\n      continue-on-error: true\n\n    - name: NetBSD Test (Attempt 3)\n      uses: vmactions/netbsd-vm@e04aec09540429f9cebb0e7941f7cd0c0fc3b44f # v1.3.6\n      if: ${{ inputs.target == 'x86_64-unknown-netbsd' && steps.netbsd_attempt_2.outcome == 'failure' }}\n      with:\n        release: \"${{ inputs.os-version }}\"\n        envs: \"RUST_BACKTRACE CARGO_INCREMENTAL CARGO_PROFILE_DEV_DEBUG CARGO_HUSKY_DONT_INSTALL_HOOKS\"\n        usesh: true\n        run: sh ./scripts/ci/bsd_tests.sh ${{ inputs.target }}\n      id: netbsd_attempt_3\n"
  },
  {
    "path": ".github/ci/rust_version.txt",
    "content": "1.94.0\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!-- Please use this template (unless you have a very good reason not to). PRs that do not use the template may be closed. -->\n\n## Description\n\n_A description of the change, what it does, and why it was made. If relevant (e.g. UI changes), **please also provide screenshots/recordings**:_\n\n## Issue\n\n_If applicable, what issue does this address?_\n\nCloses: #<issue-number>\n\n## Testing\n\n_If relevant, please state how this was tested (including steps):_\n\n_If this change affects the program, please also indicate which platforms were tested:_\n\n- [ ] _Windows_\n- [ ] _macOS (specify version below)_\n- [ ] _Linux (specify distro below)_\n- [ ] _Other (specify below)_\n\n## Checklist\n\n_Ensure **all** of these are met:_\n\n- [ ] _If this is a code change, areas your change affects have been linted using (`cargo fmt`)_\n- [ ] _If this is a code change, your changes pass `cargo clippy --all -- -D warnings`_\n- [ ] _If this is a code change, new tests were added if relevant_\n- [ ] _If this is a code change, your changes pass `cargo test`_\n- [ ] _The change has been tested to work (see above) and doesn't appear to break other things_\n- [ ] _Documentation has been updated if needed (`README.md`, help menu, docs, configs, etc.)_\n- [ ] _There are no merge conflicts_\n- [ ] _You have reviewed the changes first_\n- [ ] _The pull request passes the provided CI pipeline_\n\n## Other\n\n_Anything else that maintainers should know about this PR:_\n"
  },
  {
    "path": ".github/workflows/bsd_vm_check.yml",
    "content": "# Run BSD VM jobs with manually-implemented retries.\n\nname: \"BSD VM Check\"\n\non:\n  workflow_call:\n    inputs:\n      os-target:\n        type: string\n        description: \"BSD target (x86_64-unknown-freebsd, x86_64-unknown-netbsd, or x86_64-unknown-openbsd)\"\n        required: true\n      os-version:\n        type: string\n        description: \"Release version\"\n        required: true\n\n# Duplicated because GHA doesn't support passing env vars through without making them all inputs or something.\nenv:\n  RUST_BACKTRACE: 1\n  CARGO_INCREMENTAL: 0\n  CARGO_PROFILE_DEV_DEBUG: 0\n  CARGO_HUSKY_DONT_INSTALL_HOOKS: true\n\njobs:\n  bsd-vm-test:\n    runs-on: ubuntu-24.04\n    timeout-minutes: 20\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 1\n\n      - name: OpenBSD Test (Attempt 1)\n        uses: vmactions/openbsd-vm@9a8e4351a4a0dc6238e7c69276dcbf6c03bea576 # v1.3.6\n        if: ${{ inputs.os-target == 'x86_64-unknown-openbsd' }}\n        with:\n          release: \"${{ inputs.os-version }}\"\n          envs: \"RUST_BACKTRACE CARGO_INCREMENTAL CARGO_PROFILE_DEV_DEBUG CARGO_HUSKY_DONT_INSTALL_HOOKS\"\n          usesh: true\n          run: sh ./scripts/ci/ci_bsd.sh ${{ inputs.os-target }}\n        id: openbsd_attempt_1\n        continue-on-error: true\n\n      - name: OpenBSD Test (Attempt 2)\n        uses: vmactions/openbsd-vm@9a8e4351a4a0dc6238e7c69276dcbf6c03bea576 # v1.3.6\n        if: ${{ inputs.os-target == 'x86_64-unknown-openbsd' && steps.openbsd_attempt_1.outcome == 'failure' }}\n        with:\n          release: \"${{ inputs.os-version }}\"\n          envs: \"RUST_BACKTRACE CARGO_INCREMENTAL CARGO_PROFILE_DEV_DEBUG CARGO_HUSKY_DONT_INSTALL_HOOKS\"\n          usesh: true\n          run: sh ./scripts/ci/ci_bsd.sh ${{ inputs.os-target }}\n        id: openbsd_attempt_2\n        continue-on-error: true\n\n      - name: OpenBSD Test (Attempt 3)\n        uses: vmactions/openbsd-vm@9a8e4351a4a0dc6238e7c69276dcbf6c03bea576 # v1.3.6\n        if: ${{ inputs.os-target == 'x86_64-unknown-openbsd' && steps.openbsd_attempt_2.outcome == 'failure' }}\n        with:\n          release: \"${{ inputs.os-version }}\"\n          envs: \"RUST_BACKTRACE CARGO_INCREMENTAL CARGO_PROFILE_DEV_DEBUG CARGO_HUSKY_DONT_INSTALL_HOOKS\"\n          usesh: true\n          run: sh ./scripts/ci/ci_bsd.sh ${{ inputs.os-target }}\n        id: openbsd_attempt_3\n\n      - name: Check result\n        if: ${{ failure() }}\n        run: exit 1\n"
  },
  {
    "path": ".github/workflows/build_releases.yml",
    "content": "# Builds the following releases:\n# - Binaries\n# - Binaries via VMs\n# - Cirrus binaries (currently just Linux 2.17)\n# - MSI installer for Windows (.msi)\n# - .deb releases\n# - .rpm releases\n#\n# Note that for musl targets, we use cross to avoid having to set up the dependencies for musl.\n#\n# TODO: Break this up into scripts instead.\n# TODO: Trigger this in CI as well if this file changes, so I don't have to spam nightly builds.\n\nname: \"build releases\"\n\non:\n  workflow_dispatch:\n  workflow_call:\n    inputs:\n      caller:\n        description: \"The calling workflow.\"\n        default: \"\"\n        required: false\n        type: string\n\nenv:\n  RUST_BACKTRACE: 1\n  CARGO_INCREMENTAL: 0\n  CARGO_PROFILE_DEV_DEBUG: 0\n  CARGO_HUSKY_DONT_INSTALL_HOOKS: true\n  COMPLETION_DIR: \"target/tmp/bottom/completion/\"\n  MANPAGE_DIR: \"target/tmp/bottom/manpage/\"\n  CROSS_VERSION: \"git:588b3c99db52b5a9c5906fab96cfadcf1bde7863\"\n\npermissions:\n  id-token: write\n  contents: read\n  attestations: write\n\njobs:\n  build-binaries:\n    name: \"Build binaries\"\n    runs-on: ${{ matrix.info.os }}\n    container: ${{ matrix.info.container }}\n    timeout-minutes: 12\n    strategy:\n      fail-fast: false\n      matrix:\n        info:\n          # ======= Supported targets =======\n          # Linux (x86-64, x86, aarch64)\n          - {\n              target: \"x86_64-unknown-linux-gnu\",\n              os: \"ubuntu-22.04\",\n              cross: false,\n              generate-other-artifacts: true,\n            }\n          - {\n              target: \"i686-unknown-linux-gnu\",\n              os: \"ubuntu-22.04\",\n              cross: true,\n            }\n          - {\n              target: \"x86_64-unknown-linux-musl\",\n              os: \"ubuntu-22.04\",\n              cross: true,\n            }\n          - {\n              target: \"i686-unknown-linux-musl\",\n              os: \"ubuntu-22.04\",\n              cross: true,\n            }\n          - {\n              target: \"aarch64-unknown-linux-gnu\",\n              os: \"ubuntu-22.04-arm\",\n              cross: false,\n            }\n          - {\n              target: \"aarch64-unknown-linux-musl\",\n              os: \"ubuntu-22.04\",\n              cross: true,\n            }\n\n          # macOS (x86-64 and aarch64)\n          - {\n              target: \"x86_64-apple-darwin\",\n              os: \"macos-15-intel\",\n              cross: false,\n            }\n          - { target: \"aarch64-apple-darwin\", os: \"macos-15\", cross: false }\n\n          # Windows (x86-64, x86)\n          - {\n              target: \"x86_64-pc-windows-msvc\",\n              os: \"windows-2022\",\n              cross: false,\n            }\n          - { target: \"i686-pc-windows-msvc\", os: \"windows-2022\", cross: false }\n          - {\n              target: \"x86_64-pc-windows-gnu\",\n              os: \"windows-2022\",\n              cross: false,\n            }\n\n          # ======= Unsupported targets =======\n          # armv7\n          - {\n              target: \"armv7-unknown-linux-gnueabihf\",\n              os: \"ubuntu-22.04\",\n              cross: true,\n            }\n          - {\n              target: \"armv7-unknown-linux-musleabihf\",\n              os: \"ubuntu-22.04\",\n              cross: true,\n            }\n\n          # PowerPC 64 LE\n          - {\n              target: \"powerpc64le-unknown-linux-gnu\",\n              os: \"ubuntu-22.04\",\n              cross: true,\n            }\n\n          # Risc-V 64gc\n          - {\n              target: \"riscv64gc-unknown-linux-gnu\",\n              os: \"ubuntu-22.04\",\n              cross: true,\n            }\n\n          # Android\n          - {\n              target: \"aarch64-linux-android\",\n              os: \"ubuntu-24.04\",\n              cross: true,\n              no-default-features: true,\n            }\n\n          # Loongarch\n          - {\n              target: \"loongarch64-unknown-linux-gnu\",\n              os: \"ubuntu-22.04\",\n              cross: true,\n            }\n\n          # Windows ARM, may promote to official.\n          - {\n              target: \"aarch64-pc-windows-msvc\",\n              os: \"windows-11-arm\",\n              cross: false,\n            }\n\n          # FreeBSD\n          - {\n              target: \"x86_64-unknown-freebsd\",\n              os: \"ubuntu-22.04\",\n              cross: true,\n            }\n\n          # NetBSD\n          - { target: \"x86_64-unknown-netbsd\", os: \"ubuntu-22.04\", cross: true }\n    steps:\n      - name: Checkout repository\n        if: matrix.info.container == ''\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 1\n\n      # TODO: Make this and the toolchain step a separate wrapper action?\n      - name: Read Rust version\n        shell: bash\n        run: |\n          VER=$(cat .github/ci/rust_version.txt)\n          echo \"RUST_VERSION=$VER\" >> $GITHUB_ENV\n          echo \"$VER\"\n\n      - name: Set up Rust toolchain\n        if: matrix.info.container == ''\n        uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9\n        with:\n          toolchain: ${{ matrix.info.rust || env.RUST_VERSION }}\n          target: ${{ matrix.info.target }}\n\n      - name: Set up Rust toolchain (non-GitHub container)\n        if: matrix.info.container != ''\n        shell: bash\n        run: |\n          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs --output rustup.sh\n          sh rustup.sh --default-toolchain stable -y\n          echo \"$HOME/.cargo/bin\" >> $GITHUB_PATH\n\n      - name: Set features\n        shell: bash\n        run: |\n          if [[ \"${{ matrix.info.no-default-features }}\" == \"true\" ]]; then\n            BUILD_FEATURES=\"--no-default-features\"\n          else\n            BUILD_FEATURES=\"--features deploy\"\n          fi\n          echo \"Will build with the following features: $BUILD_FEATURES\"\n          echo \"BUILD_FEATURES=$BUILD_FEATURES\" >> $GITHUB_ENV\n\n      - name: Build\n        uses: ClementTsang/cargo-action@2438cc5f3ba4e971289fffca2a00dedea6911f14 # v0.0.7\n        env:\n          BTM_GENERATE: true\n          BTM_BUILD_RELEASE_CALLER: ${{ inputs.caller }}\n        with:\n          command: build\n          args: --release --locked --target=${{ matrix.info.target }} ${{ env.BUILD_FEATURES }}\n          use-cross: ${{ matrix.info.cross }}\n          cross-version: ${{ matrix.info.cross-version || env.CROSS_VERSION }}\n\n      - name: Move automatically generated completion/manpage\n        shell: bash\n        run: |\n          mv \"$COMPLETION_DIR\" completion\n          mv \"$MANPAGE_DIR\" manpage\n\n      - name: Determine signing slug for Windows signing\n        id: get-signing-slug\n        if: startsWith(matrix.info.os, 'windows')\n        shell: bash\n        run: |\n          mkdir -p signed\n          if [[ ${{ inputs.caller }} == \"nightly\" ]]; then\n            echo \"slug=test-signing\" >> $GITHUB_OUTPUT\n          else\n            echo \"slug=release-signing\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: Upload unsigned Windows artifact\n        id: upload-unsigned-artifact\n        if: startsWith(matrix.info.os, 'windows')\n        uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0\n        with:\n          retention-days: 1\n          name: \"unsigned-${{ matrix.info.target }}\"\n          path: target/${{ matrix.info.target }}/release/btm.exe\n\n      - name: Sign Windows artifacts\n        if: startsWith(matrix.info.os, 'windows')\n        uses: signpath/github-action-submit-signing-request@3f9250c56651ff692d6729a2fbb0603a42d7d322 # v2.0\n        with:\n          api-token: \"${{ secrets.SIGNPATH_API_TOKEN }}\"\n          organization-id: \"06b1a1ff-74e1-4d9d-93b1-fa8180c67727\"\n          project-slug: \"bottom\"\n          signing-policy-slug: \"${{ steps.get-signing-slug.outputs.slug }}\"\n          github-artifact-id: \"${{ steps.upload-unsigned-artifact.outputs.artifact-id }}\"\n          wait-for-completion: true\n          output-artifact-directory: \"signed\"\n\n      - name: Bundle release and completion (Windows)\n        if: startsWith(matrix.info.os, 'windows')\n        shell: bash\n        run: |\n          mv signed/btm.exe btm.exe\n          7z a bottom_${{ matrix.info.target }}.zip \"btm.exe\"\n          7z a bottom_${{ matrix.info.target }}.zip \"completion\"\n          echo \"ASSET=bottom_${{ matrix.info.target }}.zip\" >> $GITHUB_ENV\n\n      - name: Bundle release and completion (Linux and macOS)\n        if: ${{ !(startsWith(matrix.info.os, 'windows')) }}\n        shell: bash\n        run: |\n          cp target/${{ matrix.info.target }}/release/btm ./btm\n          tar -czvf bottom_${{ matrix.info.target }}.tar.gz btm completion\n          echo \"ASSET=bottom_${{ matrix.info.target }}.tar.gz\" >> $GITHUB_ENV\n\n      - name: Generate artifact attestation for file\n        uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2\n        with:\n          subject-path: ${{ env.ASSET }}\n\n      - name: Create release directory for artifact, move file\n        shell: bash\n        run: |\n          mkdir release\n          mv ${{ env.ASSET }} release/\n\n      - name: Compress completion files\n        if: matrix.info.generate-other-artifacts == true\n        shell: bash\n        run: |\n          tar -C ./completion -czvf completion.tar.gz .\n          mv completion.tar.gz release/\n\n      - name: Compress manpage files\n        if: matrix.info.generate-other-artifacts == true\n        shell: bash\n        run: |\n          gzip ./manpage/btm.1\n          tar -C ./manpage -czvf manpage.tar.gz .\n          mv manpage.tar.gz release/\n\n      - name: Copy over .desktop file\n        if: matrix.info.generate-other-artifacts == true\n        shell: bash\n        run: |\n          cp ./desktop/bottom.desktop release/\n\n      - name: Save release as artifact\n        uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0\n        with:\n          retention-days: 3\n          name: \"release-${{ matrix.info.target }}\"\n          path: release\n\n  build-msi:\n    name: \"Build MSI (WiX) installer\"\n    timeout-minutes: 12\n    strategy:\n      fail-fast: false\n      matrix:\n        info:\n          - {\n              os: \"windows-2022\",\n              target: \"x86_64-pc-windows-msvc\",\n              output: \"bottom_x86_64_installer.msi\",\n            }\n          - {\n              os: \"windows-11-arm\",\n              target: \"aarch64-pc-windows-msvc\",\n              output: \"bottom_aarch64_installer.msi\",\n            }\n    runs-on: ${{ matrix.info.os }}\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 1\n\n      - name: Install Net-Framework-Core\n        shell: powershell\n        run: |\n          choco install dotnet-sdk --pre -y --no-progress;\n\n      - name: Install wixtoolset\n        shell: powershell\n        run: |\n          choco install -y wixtoolset --no-progress;\n\n      - name: Read Rust version\n        shell: bash\n        run: |\n          VER=$(cat .github/ci/rust_version.txt)\n          echo \"RUST_VERSION=$VER\" >> $GITHUB_ENV\n          echo \"$VER\"\n\n      - name: Set up Rust toolchain\n        uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9\n        with:\n          toolchain: ${{ env.RUST_VERSION }}\n          target: ${{ matrix.info.target }}\n\n      - name: Install cargo-wix\n        shell: powershell\n        run: |\n          cargo install cargo-wix --version 0.3.8 --locked\n\n      - name: Build MSI file\n        shell: powershell\n        env:\n          BTM_GENERATE: true\n        run: |\n          Import-Module \"$env:ChocolateyInstall/helpers/chocolateyInstaller.psm1\"\n          refreshenv\n          cargo wix --nocapture\n          mv bottom_installer.msi ${{ matrix.info.output }}\n\n      - name: Generate artifact attestation for file\n        uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2\n        with:\n          subject-path: ${{ matrix.info.output }}\n\n      - name: Create release directory for artifact, move files\n        shell: bash\n        run: |\n          mkdir release\n          mv ${{ matrix.info.output }} release/\n\n      - name: Save release as artifact\n        uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0\n        with:\n          retention-days: 3\n          name: \"release-${{ matrix.info.target }}-msi\"\n          path: release\n\n  build-cirrus:\n    name: \"Build using Cirrus CI\"\n    runs-on: \"ubuntu-24.04\"\n    timeout-minutes: 12\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 0\n\n      - name: Create release directory\n        run: |\n          mkdir -p release\n\n      - name: Execute Cirrus CI build script\n        env:\n          CIRRUS_KEY: ${{ secrets.CIRRUS_TOKEN }}\n        run: |\n          if [[ \"${{ github.ref_type }}\" == \"branch\" ]]; then\n            BRANCH=\"${{ github.ref_name }}\";\n          else\n            raw=$(git branch -r --contains '${{ github.ref_name }}');\n            BRANCH=${raw##*/};\n          fi\n          python ./scripts/ci/cirrus_release.py \"$BRANCH\" \"release/\" \"${{ inputs.caller }}\"\n\n      - name: Generate artifact attestation for file\n        uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2\n        with:\n          subject-path: \"release/**/*.tar.gz\"\n\n      - name: Save release as artifact\n        uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0\n        with:\n          retention-days: 3\n          name: release-build-cirrus\n          path: release\n\n  build-deb:\n    name: \"Build .deb software packages\"\n    runs-on: ${{ matrix.info.os || 'ubuntu-24.04' }}\n    timeout-minutes: 12\n    strategy:\n      fail-fast: false\n      matrix:\n        info:\n          - { target: \"x86_64-unknown-linux-gnu\", dpkg: amd64 }\n          - { target: \"x86_64-unknown-linux-musl\", cross: true, dpkg: amd64 }\n          - {\n              target: \"aarch64-unknown-linux-gnu\",\n              cross: false,\n              dpkg: arm64,\n              os: \"ubuntu-22.04-arm\",\n            }\n          # The cross images don't work with ARM runners, unfortunately. We could build it on x86 with cross\n          # and then copy it over to an ARM runner but that's kind of annoying, so I'll skip this for now.\n          # TODO: Maybe improve how we do the .deb software building step?\n          - {\n              target: \"aarch64-unknown-linux-musl\",\n              cross: true,\n              dpkg: arm64,\n              container: \"ghcr.io/clementtsang/cargo-deb-aarch64-unknown-linux-gnu\",\n            }\n          - {\n              target: \"armv7-unknown-linux-gnueabihf\",\n              cross: true,\n              dpkg: armhf,\n              container: \"ghcr.io/clementtsang/cargo-deb-armv7-unknown-linux-gnueabihf\",\n            }\n          - {\n              target: \"armv7-unknown-linux-musleabihf\",\n              cross: true,\n              dpkg: armhf,\n              container: \"ghcr.io/clementtsang/cargo-deb-armv7-unknown-linux-gnueabihf\",\n            }\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 1\n\n      - name: Read Rust version\n        shell: bash\n        run: |\n          VER=$(cat .github/ci/rust_version.txt)\n          echo \"RUST_VERSION=$VER\" >> $GITHUB_ENV\n          echo \"$VER\"\n\n      - name: Set up Rust toolchain\n        uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9\n        with:\n          toolchain: ${{ matrix.info.rust || env.RUST_VERSION }}\n          target: ${{ matrix.info.target }}\n\n      - name: Build\n        uses: ClementTsang/cargo-action@2438cc5f3ba4e971289fffca2a00dedea6911f14 # v0.0.7\n        env:\n          BTM_GENERATE: true\n          BTM_BUILD_RELEASE_CALLER: ${{ inputs.caller }}\n        with:\n          command: build\n          args: --release --locked --features deploy --target ${{ matrix.info.target }}\n          use-cross: ${{ matrix.info.cross || false }}\n          cross-version: ${{ env.CROSS_VERSION }}\n\n      - name: Move automatically generated completion/manpage\n        shell: bash\n        run: |\n          mv \"$COMPLETION_DIR\" completion\n          mv \"$MANPAGE_DIR\" manpage\n\n      - name: Zip manpage\n        run: |\n          gzip ./manpage/btm.1\n\n      - name: Build Debian release (non-container)\n        if: ${{ matrix.info.container == '' }}\n        env:\n          BTM_GENERATE: true\n        run: |\n          cargo install cargo-deb --version 3.5.0 --locked\n          cargo deb --no-build --target ${{ matrix.info.target }}\n          cp ./target/debian/bottom_*.deb .\n\n      - name: Build Debian release (container)\n        if: ${{ matrix.info.container != '' }}\n        env:\n          BTM_GENERATE: true\n        run: |\n          docker pull ${{ matrix.info.container }}\n          docker run -t --rm --mount type=bind,source=\"$(pwd)\",target=/volume ${{ matrix.info.container }} \"--no-build --variant ${{ matrix.info.dpkg }} --target ${{ matrix.info.target }}\" \"/volume\"\n          cp ./target/debian/bottom-*.deb .\n          TMP_NAME=$(find bottom-*.deb)\n          VERSION=${{ matrix.info.dpkg }}\n          mv $TMP_NAME $(echo $TMP_NAME | sed \"s/-$VERSION//\")\n\n      - name: Rename if it is a musl target\n        if: contains(matrix.info.target, 'musl')\n        run: |\n          TMP_NAME=$(find bottom_*.deb)\n          mv $TMP_NAME $(echo $TMP_NAME | sed \"s/bottom/bottom-musl/\")\n\n      # TODO: Maybe rename version if nightly?\n      - name: Verify Debian release\n        id: verify\n        run: |\n          DEB_FILE=$(find bottom*_*.deb)\n          dpkg -I $DEB_FILE\n          dpkg -I $DEB_FILE | grep ${{ matrix.info.dpkg }} && echo \"Found correct architecture\"\n          echo \"DEB_FILE=$DEB_FILE\" >> $GITHUB_OUTPUT\n\n      - name: Delete generated Debian folder\n        run: |\n          sudo chown $USER ./target/debian/ 2>/dev/null || true\n          rm -r ./target/debian/\n\n      - name: Generate artifact attestation for file\n        uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2\n        with:\n          subject-path: ${{ steps.verify.outputs.DEB_FILE }}\n\n      - name: Create release directory for artifact, move file\n        shell: bash\n        run: |\n          mkdir release\n          mv ${{ steps.verify.outputs.DEB_FILE }} release/\n\n      - name: Save release as artifact\n        uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0\n        with:\n          retention-days: 3\n          name: release-build-deb-${{ matrix.info.target }}\n          path: release\n\n  build-rpm:\n    name: \"Build .rpm software packages\"\n    runs-on: ubuntu-24.04\n    container: ghcr.io/clementtsang/almalinux-8\n    timeout-minutes: 12\n    strategy:\n      fail-fast: false\n      matrix:\n        info:\n          - { target: \"x86_64-unknown-linux-gnu\" }\n          - { target: \"x86_64-unknown-linux-musl\", cross: true }\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 1\n\n      - name: Read Rust version\n        shell: bash\n        run: |\n          VER=$(cat .github/ci/rust_version.txt)\n          echo \"RUST_VERSION=$VER\" >> $GITHUB_ENV\n          echo \"$VER\"\n\n      - name: Set up Rust toolchain\n        uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9\n        with:\n          toolchain: ${{ matrix.info.rust || env.RUST_VERSION }}\n          target: ${{ matrix.info.target }}\n\n      - name: Build\n        uses: ClementTsang/cargo-action@2438cc5f3ba4e971289fffca2a00dedea6911f14 # v0.0.7\n        env:\n          BTM_GENERATE: true\n          BTM_BUILD_RELEASE_CALLER: ${{ inputs.caller }}\n          CROSS_CONTAINER_IN_CONTAINER: true\n        with:\n          command: build\n          use-cross: ${{ matrix.info.cross || false }}\n          args: --release --locked --features deploy --target ${{ matrix.info.target }}\n          cross-version: ${{ env.CROSS_VERSION }}\n\n      - name: Move automatically generated completion/manpage\n        shell: bash\n        run: |\n          mv \"$COMPLETION_DIR\" completion\n          mv \"$MANPAGE_DIR\" manpage\n\n      - name: Zip manpage\n        run: |\n          gzip ./manpage/btm.1\n\n      - name: Build rpm release\n        env:\n          BTM_GENERATE: true\n        run: |\n          cargo install cargo-generate-rpm --version 0.20.0 --locked\n          cargo generate-rpm --target ${{ matrix.info.target }}\n          cp ./target/${{ matrix.info.target }}/generate-rpm/bottom-*.rpm .\n\n      - name: Rename if it is a musl target\n        if: contains(matrix.info.target, 'musl')\n        run: |\n          TMP_NAME=$(find bottom-*.rpm)\n          mv $TMP_NAME $(echo $TMP_NAME | sed \"s/bottom/bottom-musl/\")\n\n      - name: Verify generated rpm file\n        id: verify\n        run: |\n          RPM_FILE=$(find bottom-*.rpm)\n          rpm -qip $RPM_FILE\n          # Save for future jobs\n          echo \"RPM_FILE=$RPM_FILE\" >> $GITHUB_OUTPUT\n\n      - name: Check generated rpm file signatures\n        run: |\n          rpm --checksig --verbose ${{ steps.verify.outputs.RPM_FILE }}\n          # Validate modern signature digest exist, see https://github.com/ClementTsang/bottom/issues/1848\n          rpm --checksig --verbose ${{ steps.verify.outputs.RPM_FILE }} | grep -q \"SHA256\"\n\n      - name: Test installing generated rpm file\n        run: |\n          yum localinstall -y ${{ steps.verify.outputs.RPM_FILE }}\n          btm -V\n\n      - name: Delete generated rpm folder\n        run: |\n          sudo chown $USER ./target/${{ matrix.info.target }}/generate-rpm/ 2>/dev/null || true\n          rm -r ./target/${{ matrix.info.target }}/generate-rpm/\n\n      - name: Generate artifact attestation for file\n        uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2\n        with:\n          subject-path: ${{ steps.verify.outputs.RPM_FILE }}\n\n      - name: Create release directory for artifact, move file\n        shell: bash\n        run: |\n          mkdir release\n          mv ${{ steps.verify.outputs.RPM_FILE }} release/\n\n      - name: Save release as artifact\n        uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0\n        with:\n          retention-days: 3\n          name: release-build-rpm-${{ matrix.info.target }}\n          path: release\n\n  test-installing:\n    name: \"Test Installing\"\n    runs-on: ${{ matrix.info.os }}\n    container: ${{ matrix.info.container }}\n    timeout-minutes: 12\n    strategy:\n      fail-fast: false\n      matrix:\n        info:\n          - { os: \"ubuntu-22.04\", target: \"x86_64-unknown-linux-gnu\" }\n          - { os: \"macos-15\", target: \"aarch64-apple-darwin\" }\n          - { os: \"windows-2022\", target: \"x86_64-pc-windows-msvc\" }\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 1\n\n      - name: Read Rust version\n        shell: bash\n        run: |\n          VER=$(cat .github/ci/rust_version.txt)\n          echo \"RUST_VERSION=$VER\" >> $GITHUB_ENV\n          echo \"$VER\"\n\n      - name: Set up Rust toolchain\n        uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9\n        with:\n          toolchain: ${{ matrix.info.rust || env.RUST_VERSION }}\n          target: ${{ matrix.info.target }}\n\n      - name: Install (locked)\n        shell: bash\n        run: |\n          cargo install --path . --locked\n          btm -V\n          cargo uninstall bottom\n\n      - name: Install (not locked)\n        shell: bash\n        run: |\n          cargo install --path .\n          btm -V\n          cargo uninstall bottom\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "# Main CI workflow to validate that files are formatted correctly, pass tests,\n# and pass lints.\n#\n# CI workflow was based on a lot of work from other people:\n# - https://github.com/heim-rs/heim/blob/master/.github/workflows/ci.yml\n# - https://github.com/BurntSushi/ripgrep/blob/master/.github/workflows/ci.yml\n# - https://www.reillywood.com/blog/rust-faster-ci/\n# - https://matklad.github.io/2021/09/04/fast-rust-builds.html\n\nname: ci\n\non:\n  workflow_dispatch:\n  pull_request:\n  push:\n    branches:\n      - main\n\nenv:\n  RUST_BACKTRACE: 1\n  CARGO_INCREMENTAL: 0\n  CARGO_PROFILE_DEV_DEBUG: 0\n  CARGO_HUSKY_DONT_INSTALL_HOOKS: true\n  COMPLETION_DIR: \"target/tmp/bottom/completion/\"\n  MANPAGE_DIR: \"target/tmp/bottom/manpage/\"\n  CROSS_VERSION: \"git:588b3c99db52b5a9c5906fab96cfadcf1bde7863\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: ${{ github.event_name == 'pull_request' || github.repository != 'ClementTsang/bottom' }}\n\njobs:\n  # Check if things should be skipped.\n  pre-job:\n    runs-on: ubuntu-24.04\n    outputs:\n      should_skip: ${{ steps.skip_check.outputs.should_skip }}\n    steps:\n      - name: Check if this action should be skipped\n        id: skip_check\n        uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1\n        with:\n          skip_after_successful_duplicate: \"true\"\n          paths: '[\".cargo/**\", \".github/workflows/ci.yml\", \".github/ci/**\", \"sample_configs/**\", \"src/**\", \"tests/**\", \"build.rs\", \"Cargo.lock\", \"Cargo.toml\", \"clippy.toml\", \"rustfmt.toml\", \"Cross.toml\"]'\n          do_not_skip: '[\"workflow_dispatch\", \"push\"]'\n\n  # Runs rustfmt + tests + clippy on the main supported platforms.\n  supported:\n    needs: pre-job\n    if: ${{ needs.pre-job.outputs.should_skip != 'true' }}\n    runs-on: ${{ matrix.info.os }}\n    timeout-minutes: 12\n    strategy:\n      fail-fast: false\n      matrix:\n        info:\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"x86_64-unknown-linux-gnu\",\n              cross: false,\n            }\n          - {\n              os: \"ubuntu-24.04-arm\",\n              target: \"aarch64-unknown-linux-gnu\",\n              cross: false,\n            }\n          - { os: \"macos-15\", target: \"x86_64-apple-darwin\", cross: false }\n          - { os: \"macos-15\", target: \"aarch64-apple-darwin\", cross: false }\n          - {\n              os: \"windows-2022\",\n              target: \"x86_64-pc-windows-msvc\",\n              cross: false,\n            }\n        features: [\"--all-features\", \"--no-default-features\"]\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n\n      - name: Read Rust version\n        shell: bash\n        run: |\n          VER=$(cat .github/ci/rust_version.txt)\n          echo \"RUST_VERSION=$VER\" >> $GITHUB_ENV\n          echo \"$VER\"\n\n      - name: Set up Rust toolchain\n        uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9\n        with:\n          toolchain: ${{ env.RUST_VERSION }}\n          components: rustfmt, clippy\n          target: ${{ matrix.info.target }}\n\n      - name: Enable Rust cache\n        uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # 2.8.1\n        if: ${{ github.event_name != 'pull_request' || ! github.event.pull_request.head.repo.fork }} # If it is a PR, only if not a fork\n        with:\n          key: ${{ matrix.info.target }}\n          cache-all-crates: true\n\n      - name: Check cargo fmt\n        run: cargo fmt --all -- --check\n\n      # TODO: add junit output using nextest for codecov (https://docs.codecov.com/docs/test-analytics)\n      - name: Run tests\n        uses: ClementTsang/cargo-action@2438cc5f3ba4e971289fffca2a00dedea6911f14 # v0.0.7\n        with:\n          command: test\n          args: --no-fail-fast --locked ${{ matrix.features }} --target=${{ matrix.info.target }} -- --nocapture --quiet\n          use-cross: ${{ matrix.info.cross }}\n          cross-version: ${{ env.CROSS_VERSION }}\n        env:\n          RUST_BACKTRACE: full\n\n      - name: Run clippy\n        uses: ClementTsang/cargo-action@2438cc5f3ba4e971289fffca2a00dedea6911f14 # v0.0.7\n        with:\n          command: clippy\n          args: ${{ matrix.features }} --all-targets --workspace --locked --target=${{ matrix.info.target }} -- -D warnings\n          use-cross: ${{ matrix.info.cross }}\n          cross-version: ${{ env.CROSS_VERSION }}\n        env:\n          RUST_BACKTRACE: full\n\n  # Run checks for unsupported platforms.\n  #\n  # Each target specifies a list of checks to run via the 'checks' array property.\n  # Available checks:\n  #   - format: Run cargo fmt --check\n  #   - clippy: Clippy with all features (warnings allowed)\n  #   - clippy-deny: Clippy with all features (deny on warnings, implies clippy)\n  #   - clippy-no-features: Clippy with no features (warnings allowed)\n  #   - test: Run cargo test\n  #\n  # Note: 'clippy-deny' implies 'clippy' and is mutually exclusive with it.\n  # Also, 'clippy' and 'clippy-deny' cannot be combined with 'clippy-no-features'.\n  #\n  # TODO: Maybe some of these should be allowed to fail? If so, I guess we can add back the \"unofficial\" MSRV,\n  # I would also put android there.\n  unsupported-check:\n    needs: pre-job\n    strategy:\n      fail-fast: false\n      matrix:\n        info:\n          # x86 or x86-64\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"i686-unknown-linux-gnu\",\n              cross: true,\n              checks: [\"format\", \"clippy-deny\", \"test\"],\n            }\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"x86_64-unknown-linux-musl\",\n              cross: false,\n              checks: [\"format\", \"clippy-deny\", \"test\"],\n            }\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"i686-unknown-linux-musl\",\n              cross: true,\n              checks: [\"format\", \"clippy-deny\", \"test\"],\n            }\n\n          - {\n              os: \"windows-2022\",\n              target: \"i686-pc-windows-msvc\",\n              cross: false,\n              checks: [\"format\", \"clippy-deny\", \"test\"],\n            }\n          - {\n              os: \"windows-2022\",\n              target: \"x86_64-pc-windows-gnu\",\n              cross: false,\n              checks: [\"format\", \"clippy-deny\", \"test\"],\n            }\n\n          # Windows ARM\n          # TODO: Promote to official?\n          - {\n              os: \"windows-11-arm\",\n              target: \"aarch64-pc-windows-msvc\",\n              cross: false,\n              checks: [\"format\", \"clippy-deny\", \"test\"],\n            }\n\n          # Beta\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"x86_64-unknown-linux-gnu\",\n              cross: false,\n              rust: \"beta\",\n              checks: [\"format\", \"clippy-deny\", \"test\"],\n            }\n          - {\n              os: \"macos-15\",\n              target: \"aarch64-apple-darwin\",\n              cross: false,\n              rust: \"beta\",\n              checks: [\"format\", \"clippy-deny\", \"test\"],\n            }\n          - {\n              os: \"windows-2022\",\n              target: \"x86_64-pc-windows-msvc\",\n              cross: false,\n              rust: \"beta\",\n              checks: [\"format\", \"clippy-deny\", \"test\"],\n            }\n\n          # armv7\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"armv7-unknown-linux-gnueabihf\",\n              cross: true,\n              checks: [\"clippy-no-features\"],\n            }\n\n          # armv6\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"arm-unknown-linux-gnueabihf\",\n              cross: true,\n              checks: [\"clippy-no-features\"],\n            }\n\n          # PowerPC 64 LE\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"powerpc64le-unknown-linux-gnu\",\n              cross: true,\n              checks: [\"clippy-no-features\"],\n            }\n\n          # Risc-V 64gc\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"riscv64gc-unknown-linux-gnu\",\n              cross: true,\n              checks: [\"clippy-no-features\"],\n            }\n\n          # Loongarch\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"loongarch64-unknown-linux-gnu\",\n              cross: true,\n              checks: [\"clippy-no-features\"],\n            }\n\n          # Android ARM64\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"aarch64-linux-android\",\n              cross: true,\n              checks: [\"clippy-no-features\"],\n            }\n\n          # FreeBSD\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"x86_64-unknown-freebsd\",\n              cross: true,\n              os-version: \"15.0\",\n              checks: [\"format\", \"clippy\", \"test\"],\n            }\n\n          # NetBSD\n          - {\n              os: \"ubuntu-24.04\",\n              target: \"x86_64-unknown-netbsd\",\n              cross: true,\n              os-version: \"10.1\",\n              checks: [\"clippy\", \"test\"],\n            }\n    runs-on: ${{ matrix.info.os }}\n    if: ${{ needs.pre-job.outputs.should_skip != 'true' }}\n    timeout-minutes: 12\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n\n      - name: Read Rust version\n        shell: bash\n        run: |\n          VER=$(cat .github/ci/rust_version.txt)\n          echo \"RUST_VERSION=$VER\" >> $GITHUB_ENV\n          echo \"$VER\"\n\n      - name: Set up Rust toolchain\n        uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9\n        with:\n          toolchain: ${{ matrix.info.rust || env.RUST_VERSION }}\n          target: ${{ matrix.info.target }}\n          components: ${{ contains(matrix.info.checks, 'format') && 'rustfmt, clippy' || 'clippy' }}\n\n      - name: Enable Rust cache\n        uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # 2.8.1\n        if: ${{ github.event_name != 'pull_request' || ! github.event.pull_request.head.repo.fork }} # If it is a PR, only if not a fork\n        with:\n          key: ${{ matrix.info.target }}\n          cache-all-crates: true\n\n      - name: Check cargo fmt\n        if: ${{ contains(matrix.info.checks, 'format') }}\n        run: cargo fmt --all -- --check\n\n      - name: Run tests\n        uses: ClementTsang/cargo-action@2438cc5f3ba4e971289fffca2a00dedea6911f14 # v0.0.7\n        if: ${{ contains(matrix.info.checks, 'test') && !contains(matrix.info.target, 'bsd') }}\n        with:\n          command: test\n          args: --no-fail-fast --locked --target=${{ matrix.info.target }} -- --nocapture --quiet\n          use-cross: ${{ matrix.info.cross }}\n          cross-version: ${{ matrix.info.cross-version || env.CROSS_VERSION }}\n        env:\n          RUST_BACKTRACE: full\n\n      # *BSD targets run tests in a VM due to cross limitations.\n      - name: Run tests (BSD)\n        if: ${{ contains(matrix.info.checks, 'test') && contains(matrix.info.target, 'bsd') }}\n        uses: ./.github/actions/test-bsd-target\n        with:\n          target: ${{ matrix.info.target }}\n          os-version: ${{ matrix.info.os-version }}\n\n      - name: Set up clippy configuration\n        shell: bash\n        run: |\n          if [[ \"${{ contains(matrix.info.checks, 'clippy') && contains(matrix.info.checks, 'clippy-deny') }}\" == \"true\" ]]; then\n            echo \"Error: Cannot have both 'clippy' and 'clippy-deny'. 'clippy-deny' implies 'clippy' with stricter warnings.\"\n            exit 1\n          fi\n\n          if [[ \"${{ contains(matrix.info.checks, 'clippy-deny') && contains(matrix.info.checks, 'clippy-no-features') }}\" == \"true\" ]]; then\n            echo \"Error: Cannot have both 'clippy-deny' (all-features) and 'clippy-no-features'. They are mutually exclusive.\"\n            exit 1\n          fi\n\n          if [[ \"${{ contains(matrix.info.checks, 'clippy') && contains(matrix.info.checks, 'clippy-no-features') }}\" == \"true\" ]]; then\n            echo \"Error: Cannot have both 'clippy' (all-features) and 'clippy-no-features' in the same checks array. They are mutually exclusive.\"\n            exit 1\n          fi\n\n          # Determine deny vs warn flags\n          if [[ \"${{ contains(matrix.info.checks, 'clippy-deny') }}\" == \"true\" ]]; then\n            echo \"CLIPPY_FLAGS_EXTRA=-D warnings\" >> $GITHUB_ENV\n          else\n            echo \"CLIPPY_FLAGS_EXTRA=\" >> $GITHUB_ENV\n          fi\n\n          # Determine all-features (default) vs no-features\n          if [[ \"${{ contains(matrix.info.checks, 'clippy-no-features') }}\" == \"true\" ]]; then\n            echo \"FEATURE_FLAGS=--no-default-features\" >> $GITHUB_ENV\n          else\n            echo \"FEATURE_FLAGS=\" >> $GITHUB_ENV\n          fi\n\n      - name: Run clippy\n        uses: ClementTsang/cargo-action@2438cc5f3ba4e971289fffca2a00dedea6911f14 # v0.0.7\n        if: ${{ contains(matrix.info.checks, 'clippy') || contains(matrix.info.checks, 'clippy-deny') || contains(matrix.info.checks, 'clippy-no-features') }}\n        with:\n          command: clippy\n          args: --all-targets --workspace --target=${{ matrix.info.target }} --locked ${{ env.FEATURE_FLAGS }} -- ${{ env.CLIPPY_FLAGS_EXTRA }}\n          use-cross: ${{ matrix.info.cross }}\n          cross-version: ${{ matrix.info.cross-version || env.CROSS_VERSION }}\n\n  # # Check BSD platforms using a VM layer.\n  # check-bsd-vm:\n  #   needs: pre-job\n  #   if: ${{ needs.pre-job.outputs.should_skip != 'true' }}\n  #   strategy:\n  #     fail-fast: false\n  #     matrix:\n  #       info:\n  #         # OpenBSD is not very well-supported, given that it's tier 3. We skip clippy and only run basic tests + fmt.\n  #         # We also use `--no-default-features` when building as starship-battery does not support OpenBSD.\n  #         # Cross also doesn't support OpenBSD, so we will do it with VMs here too.\n  #         - { os_release: \"7.8\", target: \"x86_64-unknown-openbsd\" } # Supports Rust 1.90\n  #   uses: ./.github/workflows/bsd_vm_check.yml\n  #   with:\n  #     os-target: ${{ matrix.info.target }}\n  #     os-version: ${{ matrix.info.os_release }}\n\n  completion:\n    name: \"CI Pass Check\"\n    needs: [supported, unsupported-check]\n    if: ${{ needs.supported.result != 'skipped' || needs.unsupported-check.result != 'skipped' }}\n    runs-on: \"ubuntu-24.04\"\n    steps:\n      - name: CI Passed\n        if: ${{ (needs.supported.result == 'success' || needs.supported.result == 'skipped') && (needs.unsupported-check.result == 'success' || needs.unsupported-check.result == 'skipped') }}\n        run: |\n          echo \"CI workflow completed successfully.\";\n\n      - name: CI Failed\n        if: ${{ needs.supported.result == 'failure' || needs.unsupported-check.result == 'failure' }}\n        run: |\n          echo \"CI workflow failed.\";\n          exit 1;\n\n      - name: CI Cancelled\n        if: ${{ needs.supported.result == 'cancelled' || needs.unsupported-check.result == 'cancelled' }}\n        run: |\n          echo \"CI workflow was cancelled.\";\n          exit 1;\n"
  },
  {
    "path": ".github/workflows/clear_workflow_cache.yml",
    "content": "# Simple job to clear the cache used by a workflow. This automatically runs when a PR is closed/merged\n# to clean up the corresponding PR's cache.\n\nname: \"clear workflow cache\"\n\non:\n  workflow_dispatch:\n    inputs:\n      id:\n        description: \"Which id to clear. Type main/master/all to clean all, and keep-main/keep-master to clean all but the main branch.\"\n        required: false\n  pull_request:\n    types:\n      - closed\n  schedule:\n    - cron: \"0 11 * * 0\"\n\njobs:\n  clear-cache:\n    if: ${{ github.event_name != 'pull_request' || ! github.event.pull_request.head.repo.fork }} # If it is a PR, only if not a fork\n    runs-on: ubuntu-24.04\n    env:\n      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 1\n\n      # We run each script twice with a small delay in between to try and catch everything.\n      - name: Clear cache\n        run: |\n          if [[ -n \"${{ github.event.schedule }}\" ]]; then\n            python ./scripts/clear_cache.py keep-main\n            sleep 5\n            python ./scripts/clear_cache.py keep-main\n          elif [[ -z \"${{ github.event.inputs.id }}\" ]]; then\n            python ./scripts/clear_cache.py ${{ github.event.pull_request.number }}\n            sleep 5\n            python ./scripts/clear_cache.py ${{ github.event.pull_request.number }}\n          else\n            python ./scripts/clear_cache.py ${{ github.event.inputs.id }}\n            sleep 5\n            python ./scripts/clear_cache.py ${{ github.event.inputs.id }}\n          fi\n"
  },
  {
    "path": ".github/workflows/coverage.yml",
    "content": "# Code coverage generation via cargo-llvm-cov, which is then uploaded to Codecov.\n# Codecov will report back via a comment if run on a PR.\n#\n# Note that Codecov will report back the average all uploaded coverage files.\n\nname: codecov\n\non:\n  workflow_dispatch:\n  pull_request:\n  push:\n    branches:\n      - main\n\nenv:\n  CARGO_INCREMENTAL: 0\n  CARGO_HUSKY_DONT_INSTALL_HOOKS: true\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: ${{ github.event_name == 'pull_request' || github.repository != 'ClementTsang/bottom' }}\n\njobs:\n  pre-job:\n    runs-on: ubuntu-24.04\n    outputs:\n      should_skip: ${{ steps.skip_check.outputs.should_skip }}\n    steps:\n      - id: skip_check\n        uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1\n        with:\n          skip_after_successful_duplicate: \"false\"\n          paths: '[\"tests/**\", \"src/**\", \".github/workflows/coverage.yml\", \".github/ci\", \".cargo/**\", \"Cargo.toml\", \"Cargo.lock\", \"build.rs\"]'\n          do_not_skip: '[\"workflow_dispatch\", \"push\"]'\n\n  coverage:\n    needs: pre-job\n    if: ${{ needs.pre-job.outputs.should_skip != 'true' }}\n    runs-on: ${{ matrix.info.os }}\n    timeout-minutes: 12\n    strategy:\n      fail-fast: false\n      matrix:\n        info:\n          - { os: \"ubuntu-24.04\", target: \"x86_64-unknown-linux-gnu\" }\n          - { os: \"macos-14\", target: \"aarch64-apple-darwin\", cross: false }\n          - { os: \"windows-2022\", target: \"x86_64-pc-windows-msvc\" }\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n\n      - name: Read Rust version\n        shell: bash\n        run: |\n          VER=$(cat .github/ci/rust_version.txt)\n          echo \"RUST_VERSION=$VER\" >> $GITHUB_ENV\n          echo \"$VER\"\n\n      - name: Set up Rust toolchain\n        uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9\n        with:\n          toolchain: ${{ env.RUST_VERSION }}\n\n      - name: Enable Rust cache\n        uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # 2.8.1\n        if: ${{ github.event_name != 'pull_request' || ! github.event.pull_request.head.repo.fork }} # If it is a PR, only if not a fork\n        with:\n          key: ${{ matrix.info.target }}\n          cache-all-crates: true\n\n      - name: Install cargo-llvm-cov\n        run: |\n          rustup component add llvm-tools-preview\n          cargo install cargo-llvm-cov --version 0.6.22 --locked\n\n      - name: Generate code coverage\n        run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info --locked --target=${{ matrix.info.target }}\n\n      # The token is generally not needed, but sometimes the default shared token hits limits.\n      # Yes this is ugly as hell. Why retrying is not a built-in feature of GHA, I have no idea.\n\n      - name: Upload to codecov.io (Attempt 1)\n        uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0\n        with:\n          files: lcov.info\n          fail_ci_if_error: true\n          token: ${{ secrets.CODECOV_TOKEN }}\n          flags: ${{ matrix.info.os }}\n        id: upload_attempt_1\n        continue-on-error: true\n\n      - name: Upload to codecov.io (Attempt 2)\n        uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0\n        with:\n          files: lcov.info\n          fail_ci_if_error: true\n          token: ${{ secrets.CODECOV_TOKEN }}\n          flags: ${{ matrix.info.os }}\n        if: steps.upload_attempt_1.outcome == 'failure'\n        id: upload_attempt_2\n        continue-on-error: true\n\n      - name: Upload to codecov.io (Attempt 3)\n        uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0\n        with:\n          files: lcov.info\n          fail_ci_if_error: true\n          token: ${{ secrets.CODECOV_TOKEN }}\n          flags: ${{ matrix.info.os }}\n        if: steps.upload_attempt_2.outcome == 'failure'\n        id: upload_attempt_3\n"
  },
  {
    "path": ".github/workflows/deployment.yml",
    "content": "# How we deploy a release. Covers binary builds. Also manages packaging for choco.\n#\n# Binaries are primarily built by GHA, though some Linux, M1 macOS, and FreeBSD builds are\n# handled by CirrusCI.\n\nname: deployment\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"Which tag to deploy as:\"\n        required: true\n  push:\n    tags:\n      - \"[0-9]+.[0-9]+.[0-9]+\"\n\nenv:\n  CARGO_INCREMENTAL: 0\n  CARGO_PROFILE_DEV_DEBUG: 0\n  CARGO_HUSKY_DONT_INSTALL_HOOKS: true\n\njobs:\n  initialize:\n    name: initialize\n    runs-on: ubuntu-24.04\n    outputs:\n      version: ${{ env.VERSION }}\n    steps:\n      - name: Get the release version from the tag\n        if: env.VERSION == ''\n        run: |\n          if [[ -n \"${{ github.event.inputs.tag }}\" ]]; then\n            echo \"Manual run against a tag; overriding actual tag in the environment...\"\n            echo \"VERSION=${{ github.event.inputs.tag }}\" >> $GITHUB_ENV\n          else\n            echo \"VERSION=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV\n          fi\n\n      - name: Validate version environment variable\n        run: |\n          echo \"Version being built against is version ${{ env.VERSION }}\"!\n\n  build-release:\n    needs: [initialize]\n    uses: ./.github/workflows/build_releases.yml\n    with:\n      caller: \"deployment\"\n    secrets: inherit\n\n  generate-choco:\n    needs: [initialize, build-release]\n    name: \"Generate Chocolatey files\"\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 1\n\n      - name: Set release version\n        shell: bash\n        run: |\n          echo \"RELEASE_VERSION=${{ needs.initialize.outputs.version }}\" >> $GITHUB_ENV\n\n      - name: Validate release version\n        run: |\n          echo \"Release version: ${{ env.RELEASE_VERSION }}\"\n\n      - name: Get release artifacts\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8\n        with:\n          pattern: release-*\n          path: release\n          merge-multiple: true\n\n      - name: Execute choco packaging script\n        run: |\n          python \"./scripts/windows/choco/choco_packager.py\" \"./release/bottom_x86_64-pc-windows-msvc.zip\" ${{ env.RELEASE_VERSION }} \"./scripts/windows/choco/bottom.nuspec.template\" \"./scripts/windows/choco/chocolateyinstall.ps1.template\" \"bottom.nuspec\" \"tools/chocolateyinstall.ps1\" \"tools/\"\n          zip -r choco.zip \"bottom.nuspec\" \"tools\"\n\n      - name: Move release file into release directory\n        shell: bash\n        run: mv choco.zip release/\n\n      - name: Save release as artifact\n        uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0\n        with:\n          retention-days: 3\n          name: release-choco\n          path: release\n\n  upload-release:\n    name: upload-release\n    runs-on: ubuntu-24.04\n    needs: [initialize, generate-choco, build-release]\n    steps:\n      - name: Set release version\n        shell: bash\n        run: |\n          echo \"RELEASE_VERSION=${{ needs.initialize.outputs.version }}\" >> $GITHUB_ENV\n\n      - name: Validate release version\n        run: |\n          echo \"Release version: ${{ env.RELEASE_VERSION }}\"\n\n      - name: Get release artifacts\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8\n        with:\n          pattern: release-*\n          path: release\n          merge-multiple: true\n\n      - name: Print out all release files\n        run: |\n          echo \"Generated $(ls ./release | wc -l) files:\"\n          du -h -d 0 ./release/*\n\n      - name: Create release and add release files\n        uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # 2.0.8\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          prerelease: false\n          tag_name: ${{ env.RELEASE_VERSION }}\n          draft: true\n          fail_on_unmatched_files: true\n          name: ${{ env.RELEASE_VERSION }} Release\n          body: |\n            <!-- Write summary here -->\n\n            ---\n\n            ## Bug Fixes\n              \n            ## Features\n              \n            ## Changes\n\n            ## Other\n\n            ## Internal Changes\n\n            ## New Contributors\n\n            ---\n\n            **Full Changelog:**\n          files: |\n            ./release/*\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "# Workflow to deploy mkdocs documentation.\n\nname: docs\n\non:\n  workflow_dispatch:\n  workflow_call:\n    inputs:\n      nightly:\n        description: \"Optional nightly redirect override\"\n        default: \"\"\n        required: false\n        type: string\n  push:\n    branches:\n      - main\n    paths:\n      - \"docs/**\"\n      - \".github/workflows/docs.yml\"\n\nenv:\n  # Assign commit authorship to official GitHub Actions bot when pushing to the `gh-pages` branch:\n  GIT_USER: \"github-actions[bot]\"\n  GIT_EMAIL: \"41898282+github-actions[bot]@users.noreply.github.com\"\n\njobs:\n  build-documentation:\n    name: Build and deploy docs\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 0\n\n      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0\n        with:\n          python-version: 3.12\n\n      - name: Install Python dependencies\n        run: pip install -r docs/requirements.txt\n\n      - name: Configure git user and email\n        run: ./scripts/ci/configure_git.sh\n\n      - name: Build and deploy docs with mike\n        env:\n          MKDOCS_NIGHTLY_RELEASE_OVERRIDE: ${{ inputs.nightly || '' }}\n        run: |\n          cd docs\n          mike deploy nightly --push\n\n  publish-gh-pages:\n    needs: [build-documentation]\n    uses: ./.github/workflows/publish_github_pages.yml\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/nightly.yml",
    "content": "# Creates nightly deployment builds for main targets. Note this does not cover package distribution channels,\n# such as choco.\n\nname: nightly\n\non:\n  schedule:\n    - cron: \"0 0 * * *\"\n  workflow_dispatch:\n    inputs:\n      isMock:\n        description: \"Mock run\"\n        default: true\n        required: false\n        type: boolean\n\nenv:\n  CARGO_INCREMENTAL: 0\n  CARGO_PROFILE_DEV_DEBUG: 0\n  CARGO_HUSKY_DONT_INSTALL_HOOKS: true\n\njobs:\n  # Check if things should be skipped, or if this is a mock job.\n  initialize-job:\n    name: initialize-job\n    runs-on: ubuntu-24.04\n    outputs:\n      should_skip: ${{ steps.skip_check.outputs.should_skip }}\n    steps:\n      - name: Check if this action should be skipped\n        id: skip_check\n        uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1\n        with:\n          skip_after_successful_duplicate: \"true\"\n          do_not_skip: '[\"workflow_dispatch\"]'\n\n      - name: Check if mock\n        run: |\n          if [[ -z \"${{ github.event.inputs.isMock }}\" ]]; then\n            echo \"This is a scheduled nightly run.\"\n          elif [[ \"${{ github.event.inputs.isMock }}\" == \"true\" ]]; then\n            echo \"This is a mock run.\"\n          else\n            echo \"This is NOT a mock run. Watch for the generated files!\"\n          fi\n\n  build-release:\n    needs: initialize-job\n    if: ${{ needs.initialize-job.outputs.should_skip != 'true' }}\n    uses: ./.github/workflows/build_releases.yml\n    with:\n      caller: \"nightly\"\n    secrets: inherit\n\n  upload-release:\n    name: upload-release\n    needs: build-release\n    runs-on: ubuntu-24.04\n    outputs:\n      TAG_NAME: ${{ steps.tag_release_name.outputs.TAG_NAME }}\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 1\n\n      - name: Get release artifacts\n        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8\n        with:\n          pattern: release-*\n          path: release\n          merge-multiple: true\n\n      - name: Print out all release files\n        run: |\n          echo \"Generated $(ls ./release | wc -l) files:\"\n          du -h -d 0 ./release/*\n\n      - name: Create and set tag name and release name\n        if: github.event.inputs.isMock != 'true'\n        id: tag_release_name\n        run: |\n          COMMIT_HASH=$(git rev-parse --short=8 HEAD)\n          TIME=$(date +%s)\n          TAG_NAME=$(echo \"nightly-$COMMIT_HASH-$TIME\")\n          echo \"TAG_NAME=$TAG_NAME\" >> $GITHUB_ENV\n          echo \"TAG_NAME=$$TAG_NAME\" >> \"$GITHUB_OUTPUT\"\n          echo \"$TAG_NAME\"\n\n          DATE=$(date '+%Y-%m-%d')\n          RELEASE_NAME=$(echo \"Nightly ($DATE)\")\n          echo \"RELEASE_NAME=$RELEASE_NAME\" >> $GITHUB_ENV\n          echo \"$RELEASE_NAME\"\n\n      # Delete all but last three nightly runs, as well as nightly runs that are a duplicate of today.\n      - name: Delete old tags and release if not mock\n        if: github.event.inputs.isMock != 'true'\n        run: |\n          echo \"Deleting any nightly runs with the same name as today's nightly run...\"\n          while true; do\n            TO_DELETE_NIGHTLY=$(gh release list --json name,tagName,publishedAt | jq --arg RELEASE_NAME \"$RELEASE_NAME\" -c '[.[] | select (.tagName | contains(\"nightly-\")) | select (.name == $RELEASE_NAME)][0] | .tagName' | tr -d '\"')\n            if [[ \"$TO_DELETE_NIGHTLY\" != \"null\" && \"$TO_DELETE_NIGHTLY\" == *\"nightly-\"* ]]; then\n              echo \"Will delete nightly release with tag '$TO_DELETE_NIGHTLY'\";\n              gh release delete $TO_DELETE_NIGHTLY --cleanup-tag || { echo \"couldn't delete previous nightly release, halting\"; break; }\n            else\n              echo \"no nightly releases left, done\";\n              break;\n            fi\n          done\n\n          echo \"Now pruning all but the last two nightly runs (so we end up with 3 releases after)...\"\n          while true; do\n            # Very gross trick - keep deleting the 3rd nightly element ([2]) until null.\n            TO_DELETE_NIGHTLY=$(gh release list --json name,tagName,publishedAt | jq --arg RELEASE_NAME \"$RELEASE_NAME\" -c '[.[] | select (.tagName | contains(\"nightly-\"))][2] | .tagName' | tr -d '\"')\n            if [[ \"$TO_DELETE_NIGHTLY\" != \"null\" && \"$TO_DELETE_NIGHTLY\" == *\"nightly-\"* ]]; then\n              echo \"Will delete nightly release with tag '$TO_DELETE_NIGHTLY'\";\n              gh release delete $TO_DELETE_NIGHTLY --cleanup-tag || { echo \"couldn't delete previous nightly release, halting\"; break; }\n            else\n              echo \"no nightly releases left, done\";\n              break;\n            fi\n          done\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      # As a workaround to immutable releases, we create it as a draft first, then manually publish it after.\n      - name: Add all release files and create nightly release if not mock\n        uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # 2.0.8\n        if: github.event.inputs.isMock != 'true'\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          prerelease: true\n          tag_name: ${{ env.TAG_NAME }}\n          draft: true\n          fail_on_unmatched_files: true\n          name: ${{ env.RELEASE_NAME }}\n          files: |\n            ./release/*\n\n      - name: Publish the draft release\n        if: github.event.inputs.isMock != 'true'\n        run: gh release edit \"$TAG_NAME\" --draft=false\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  docs:\n    needs: [initialize-job, upload-release]\n    if: ${{ needs.initialize-job.outputs.should_skip != 'true' && github.event.inputs.isMock != 'true' }}\n    uses: ./.github/workflows/docs.yml\n    secrets: inherit\n    with:\n      nightly: ${{needs.job1.outputs.output1}}\n"
  },
  {
    "path": ".github/workflows/post_release.yml",
    "content": "# Actions to run after releasing a version, like:\n# - Generating documentation via mkdocs\n# - Notifying packaging repos\n\nname: post-release\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: \"Which tag to deploy as:\"\n        required: true\n\nenv:\n  # Assign commit authorship to official GitHub Actions bot when pushing to the `gh-pages` branch:\n  GIT_USER: \"github-actions[bot]\"\n  GIT_EMAIL: \"41898282+github-actions[bot]@users.noreply.github.com\"\n\njobs:\n  initialize:\n    name: initialize\n    runs-on: ubuntu-24.04\n    outputs:\n      version: ${{ env.VERSION }}\n    steps:\n      - name: Get the release version from the tag\n        run: |\n          if [[ -n \"${{ github.event.inputs.tag }}\" ]]; then\n            echo \"Manual run against a tag; overriding actual tag in the environment...\"\n            echo \"VERSION=${{ github.event.inputs.tag }}\" >> \"$GITHUB_ENV\"\n          else\n            echo \"VERSION=${{ github.event.release.tag_name }}\" >> \"$GITHUB_ENV\"\n          fi\n\n      - name: Make sure you're not on master/main/nightly\n        run: |\n          echo ${{ env.VERSION }}\n          if [[ ${{ env.VERSION }} == \"master\" || ${{ env.VERSION }}  == \"main\" || ${{ env.VERSION }}  == \"nightly\" ]]; then\n            exit 1\n          fi\n\n  docs:\n    needs: [initialize]\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Set release version\n        shell: bash\n        run: |\n          echo \"RELEASE_VERSION=${{ needs.initialize.outputs.version }}\" >> $GITHUB_ENV\n\n      - name: Validate release version\n        run: |\n          echo \"Release version: ${{ env.RELEASE_VERSION }}\"\n\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 0\n\n      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0\n        with:\n          python-version: 3.12\n\n      - name: Install Python dependencies\n        run: pip install -r docs/requirements.txt\n\n      - name: Configure git user and email\n        run: ./scripts/ci/configure_git.sh\n\n      - name: Build and deploy docs with mike as the latest stable branch\n        run: |\n          cd docs\n          mike deploy --push --update-aliases ${RELEASE_VERSION} stable\n\n  publish-gh-pages:\n    needs: [docs]\n    uses: ./.github/workflows/publish_github_pages.yml\n    secrets: inherit\n\n  chocolatey:\n    needs: [initialize]\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Set release version\n        shell: bash\n        run: |\n          echo \"RELEASE_VERSION=${{ needs.initialize.outputs.version }}\" >> $GITHUB_ENV\n\n      - name: Validate release version\n        run: |\n          echo \"Release version: ${{ env.RELEASE_VERSION }}\"\n      - name: Trigger choco\n        run: |\n          curl -X POST https://api.github.com/repos/ClementTsang/choco-bottom/dispatches \\\n          -H 'Accept: application/vnd.github.everest-preview+json' \\\n          -u ${{ secrets.BOTTOM_PACKAGE_DEPLOYMENT }} \\\n          --data '{ \"event_type\": \"update\", \"client_payload\": { \"version\": \"'\"$RELEASE_VERSION\"'\" } }'\n"
  },
  {
    "path": ".github/workflows/publish_github_pages.yml",
    "content": "# Workflow to publish to GitHub Pages. Based on the normal\n# job (e.g. https://github.com/ClementTsang/bottom/actions/runs/19805277892),\n# but re-implemented so I can pin hashes.\n#\n# This action uses actions/upload-pages-artifact, which uses actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 (v4.6.2).\n\nname: Publish GitHub Pages\n\non:\n  workflow_dispatch:\n  workflow_call:\n\njobs:\n  build:\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          ref: gh-pages\n          submodules: recursive\n          fetch-depth: 1\n\n      - name: Upload artifact\n        id: upload\n        uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0\n        with:\n          path: \"./\"\n\n  deploy:\n    runs-on: ubuntu-24.04\n    needs: build\n    if: github.ref == 'refs/heads/main'\n    environment:\n      name: github-pages\n      url: ${{ steps.deploy.outputs.page_url }}\n    permissions:\n      pages: write\n      id-token: write\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deploy\n        uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5\n"
  },
  {
    "path": ".github/workflows/test_docs.yml",
    "content": "# Small CI workflow to test if mkdocs documentation can be successfully built.\n\nname: test docs\n\non:\n  workflow_dispatch:\n  pull_request:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: ${{ github.event_name == 'pull_request' || github.repository != 'ClementTsang/bottom' }}\n\njobs:\n  pre-job:\n    runs-on: ubuntu-24.04\n    outputs:\n      should_skip: ${{ steps.skip_check.outputs.should_skip }}\n    steps:\n      - id: skip_check\n        uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1\n        with:\n          skip_after_successful_duplicate: \"true\"\n          paths: '[\"docs/**\", \".github/workflows/docs.yml\", \".github/workflows/test_docs.yml\"]'\n          do_not_skip: '[\"workflow_dispatch\"]'\n\n  test-build-documentation:\n    name: Test building docs\n    needs: pre-job\n    if: ${{ needs.pre-job.outputs.should_skip != 'true' }}\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 0\n\n      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0\n        with:\n          python-version: 3.12\n\n      - name: Install Python dependencies\n        run: pip install -r docs/requirements.txt\n\n      - name: Build docs with mkdocs\n        run: |\n          cd docs\n          mkdocs build\n"
  },
  {
    "path": ".github/workflows/validate_schema.yml",
    "content": "# Workflow to validate the latest schema.\n\nname: \"validate schema\"\non:\n  workflow_dispatch:\n  pull_request:\n  push:\n    branches:\n      - main\n    paths:\n      - \"schema/**\"\n      - \"scripts/schema/**\"\n      - \".github/workflows/validate_schema.yml\"\n      - \"src/bin/schema.rs\"\n      - \"Cargo.toml\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: ${{ github.event_name == 'pull_request' || github.repository != 'ClementTsang/bottom' }}\n\njobs:\n  pre-job:\n    runs-on: ubuntu-24.04\n    outputs:\n      should_skip: ${{ steps.skip_check.outputs.should_skip }}\n    steps:\n      - id: skip_check\n        uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1\n        with:\n          skip_after_successful_duplicate: \"true\"\n          paths: '[\"schema/**\", \"scripts/schema/**\", \".github/workflows/validate_schema.yml\", \".github/ci\", \"src/bin/schema.rs\", \"Cargo.toml\"]'\n          do_not_skip: '[\"workflow_dispatch\"]'\n\n  test-build-documentation:\n    name: Test validating schema\n    needs: pre-job\n    if: ${{ needs.pre-job.outputs.should_skip != 'true' }}\n    runs-on: ubuntu-24.04\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n        with:\n          fetch-depth: 0\n\n      - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0\n        with:\n          python-version: 3.12\n\n      - name: Install Python dependencies\n        run: pip install -r scripts/schema/requirements.txt\n\n      - name: Test nightly validates on valid sample configs\n        run: |\n          python3 scripts/schema/validator.py -s ./schema/nightly/bottom.json -f ./sample_configs/default_config.toml\n          python3 scripts/schema/validator.py --uncomment -s ./schema/nightly/bottom.json -f ./sample_configs/default_config.toml\n          python3 scripts/schema/validator.py -s ./schema/nightly/bottom.json -f ./sample_configs/demo_config.toml\n\n      - name: Test nightly catches on a bad sample config\n        run: |\n          python3 scripts/schema/validator.py -s ./schema/nightly/bottom.json -f scripts/schema/bad_file.toml --should_fail\n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n# Logging\n*.log\n\n# Flamegraph stuff\nflamegraphs/\nrust-unmangle\n./*.svg\nflamegraph.svg\n*.data\n*.data.old\n\n# IntelliJ\n.idea/\n\n# Heaptrack files\n*.zst\n\n# For testing\nsample_configs/testing*.toml\n\n# Cargo-deny\ndeny.toml\n\n# Editors\n.vscode\n.zed\n.idea\n\n# mkdocs\nsite/\n\n# dhat heap profiling\ndhat-heap.json\ndhat/\n\n# cargo vet\nsupply-chain/\n\n# samply profiling\nprofile.json\nprofile.json.gz\n\n**/venv/\n\n# Sometimes used for scripts\n.ruff_cache\n"
  },
  {
    "path": ".markdownlint.json",
    "content": "{\n  \"MD013\": false,\n  \"MD041\": false,\n  \"MD033\": false,\n  \"MD040\": false,\n  \"MD024\": false,\n  \"MD025\": false,\n  \"MD046\": false,\n  \"MD059\": false,\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file. The format is based on\n[Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\nVersioning for this project is based on [Semantic Versioning](https://semver.org/spec/v2.0.0.html). More specifically:\n\n**Pre 1.0.0 (current)**:\n\n- Patch versions should aim to only contain bug fixes or non-breaking features/changes.\n- Minor versions may break things.\n\n**Post 1.0.0**:\n\n- Patch versions should only contain bug fixes.\n- Minor versions should only contain forward-compatible features/changes.\n- Major versions may break things.\n\nThat said, these are more guidelines rather than hard rules, though the project will generally try to follow them.\n\n---\n\n## [0.12.4]/[0.13.0] - Unreleased\n\n### Features\n\n- [#1938](https://github.com/ClementTsang/bottom/pull/1938), [#1980](https://github.com/ClementTsang/bottom/pull/1980): Report average packet size and packet rate.\n\n### Other\n\n- [#1955](https://github.com/ClementTsang/bottom/pull/1955): Fix mirrored documentation deploy to GitHub Pages.\n- [#1957](https://github.com/ClementTsang/bottom/pull/1957): Fix CI bug around deploying docs on release.\n- [#1958](https://github.com/ClementTsang/bottom/pull/1958): Fix cosmetic banner issue on docs page.\n\n## [0.12.3] - 2026-01-01\n\n### Bug Fixes\n\n- [#1943](https://github.com/ClementTsang/bottom/pull/1943): Fix a crash caused by multibyte UTF8 chars in process names.\n\n### Other\n\n- [#1939](https://github.com/ClementTsang/bottom/pull/1935): Update Fedora install instructions.\n\n## [0.12.2] - 2025-12-25\n\n### Bug Fixes\n\n- [#1933](https://github.com/ClementTsang/bottom/pull/1933): Fix a memory leak in Windows while getting process priority information.\n\n## [0.12.1] - 2025-12-25\n\n### Other\n\n- [#1920](https://github.com/ClementTsang/bottom/pull/1920), [#1921](https://github.com/ClementTsang/bottom/pull/1921):\n  Fix issues with installing via Cargo when locked dependencies aren't used.\n\n## [0.12.0] - 2025-12-25\n\n### Features\n\n- [#1830](https://github.com/ClementTsang/bottom/pull/1830): Add spacebar shortcut to toggle process tree expansion.\n- [#1861](https://github.com/ClementTsang/bottom/pull/1861): Add read-only mode, where things like killing processes is\n  disabled.\n- [#1890](https://github.com/ClementTsang/bottom/pull/1890): Add enter key shortcut to close process search widget.\n- [#1881](https://github.com/ClementTsang/bottom/pull/1881): Add nice (UNIX-only) and priority columns to the process\n  widget.\n\n### Bug Fixes\n\n- [#1910](https://github.com/ClementTsang/bottom/pull/1910): Fix a bug around quote parsing in the process widget's\n  search.\n\n### Other\n\n- [#1888](https://github.com/ClementTsang/bottom/pull/1888): Make automatically generated `.deb` package conflict with\n  the official one.\n- [#1891](https://github.com/ClementTsang/bottom/pull/1891): Fix typos in codebase.\n- [#1896](https://github.com/ClementTsang/bottom/pull/1896): Rename Linux icon to avoid collision with generic \"bottom\"\n  icon.\n- [#1913](https://github.com/ClementTsang/bottom/pull/1913): Add `loongarch64-unknown-linux-gnu` binary build target in\n  CI.\n- [#1914](https://github.com/ClementTsang/bottom/pull/1914): Add `aarch64-linux-android` binary build target in CI (with\n  no default features).\n\n## [0.11.4] - 2025-11-16\n\n### Bug Fixes\n\n- [#1859](https://github.com/ClementTsang/bottom/pull/1859): Ensure average CPU is drawn on top in \"All\" mode.\n- [#1867](https://github.com/ClementTsang/bottom/pull/1867): Fix network graph y-axis height cache not updating\n  correctly.\n- [#1867](https://github.com/ClementTsang/bottom/pull/1867): Fix network graph y-axis occasionally starting with a range\n  of zero.\n\n### Other\n\n- [#1863](https://github.com/ClementTsang/bottom/pull/1863): Replace bottom icon with a square version.\n- [#1865](https://github.com/ClementTsang/bottom/pull/1865): Improve help dialog width calculation.\n\n## [0.11.3] - 2025-11-06\n\n### Features\n\n- [#1812](https://github.com/ClementTsang/bottom/pull/1812): Add `free_arc` option to subtract ARC from total memory\n  usage.\n\n### Bug Fixes\n\n- [#1833](https://github.com/ClementTsang/bottom/pull/1833): Sort disk I/O using actual value rather than string\n  representation.\n- [#1812](https://github.com/ClementTsang/bottom/pull/1812): Fix ARC collection on FreeBSD.\n- [#1846](https://github.com/ClementTsang/bottom/pull/1846): Fix displayed average CPU value being wrong in graphs.\n\n### Other\n\n- [#1838](https://github.com/ClementTsang/bottom/pull/1838): Add icon for application.\n\n## [0.11.2] - 2025-10-07\n\n### Features\n\n- [#1793](https://github.com/ClementTsang/bottom/pull/1793): Add support for threads in Linux.\n- [#1719](https://github.com/ClementTsang/bottom/pull/1719): Support ignoring all keypresses.\n- [#1772](https://github.com/ClementTsang/bottom/pull/1772): Support hiding kernel threads.\n\n### Bug Fixes\n\n- [#1800](https://github.com/ClementTsang/bottom/pull/1800): Fix colon at end of process name in Linux.\n- [#1804](https://github.com/ClementTsang/bottom/pull/1804): Draw average CPU last again.\n- [#1811](https://github.com/ClementTsang/bottom/pull/1811): Fix drawing average CPU in basic mode when dedicated row is\n  enabled.\n- [#1817](https://github.com/ClementTsang/bottom/pull/1817): Fix builds for FreeBSD on ARM/PowerPC due to `libc::c_char`\n  data type being different.\n- [#1821](https://github.com/ClementTsang/bottom/pull/1821): Use alpha version of ratatui version which fixes drawing at\n  high resolutions.\n- [#1827](https://github.com/ClementTsang/bottom/pull/1827): Fix crash for Windows where the network widget could cause\n  a crash if the program started too quickly after boot under certain settings.\n\n### Other\n\n- [#1801](https://github.com/ClementTsang/bottom/pull/1801): Build and check Windows ARM.\n- [#1816](https://github.com/ClementTsang/bottom/pull/1816): Optimize username cloning on Unix.\n\n## [0.11.1] - 2025-08-15\n\n### Bug Fixes\n\n- [#1776](https://github.com/ClementTsang/bottom/pull/1776): Fix `disk.columns` being incorrectly interpreted as blank.\n- [#1787](https://github.com/ClementTsang/bottom/pull/1787): Fix issue with battery widget time and small widths.\n\n### Other\n\n- [#1779](https://github.com/ClementTsang/bottom/pull/1779), [#1788](https://github.com/ClementTsang/bottom/pull/1788):\n  Speed up time between startup and displaying data.\n\n## [0.11.0] - 2025-08-05\n\n### Features\n\n- [#1625](https://github.com/ClementTsang/bottom/pull/1625): Add the ability to configure the disk widget's table\n  columns.\n- [#1641](https://github.com/ClementTsang/bottom/pull/1641) + [#1692](https://github.com/ClementTsang/bottom/pull/1692):\n  Support AMD GPU data collection on Linux.\n- [#1642](https://github.com/ClementTsang/bottom/pull/1642): Support changing the widget borders.\n- [#1717](https://github.com/ClementTsang/bottom/pull/1717): Support delete key (fn + delete on macOS) to kill\n  processes.\n- [#1306](https://github.com/ClementTsang/bottom/pull/1306): Support using left/right key to collapse/expand process\n  trees respectively.\n- [#1767](https://github.com/ClementTsang/bottom/pull/1767): Add a virtual memory column for processes.\n- [#1770](https://github.com/ClementTsang/bottom/pull/1770) (\n  originally [#1627](https://github.com/ClementTsang/bottom/pull/1627)): Add option to have process tree entries be\n  collapsed by default.\n\n### Bug Fixes\n\n- [#1551](https://github.com/ClementTsang/bottom/pull/1551): Fix missing parent section names in default config.\n- [#1552](https://github.com/ClementTsang/bottom/pull/1552): Fix typo in default config.\n- [#1565](https://github.com/ClementTsang/bottom/pull/1565): Fix issue where CPU usage in basic mode looks weird if core\n  count isn't divisible by four.\n- [#1578](https://github.com/ClementTsang/bottom/pull/1578): Fix missing selected text background colour in\n  `default-light` theme.\n- [#1593](https://github.com/ClementTsang/bottom/pull/1593): Fix using `\"none\"` for chart legend position in configs.\n- [#1594](https://github.com/ClementTsang/bottom/pull/1594): Fix incorrect default config definitions for chart legends.\n- [#1596](https://github.com/ClementTsang/bottom/pull/1596): Fix support for nilfs2 file system.\n- [#1660](https://github.com/ClementTsang/bottom/pull/1660): Fix properly cleaning up the terminal if the program is\n  terminated due to an `Err` bubbling to the top.\n- [#1663](https://github.com/ClementTsang/bottom/pull/1663): Fix network graphs using log scaling having broken lines\n  when a point was 0.\n- [#1667](https://github.com/ClementTsang/bottom/pull/1667): Fix for ARC/SWAP not being hidden in basic mode after\n  refactor.\n- [#1683](https://github.com/ClementTsang/bottom/pull/1683): Fix graph lines potentially showing up behind legends.\n- [#1701](https://github.com/ClementTsang/bottom/pull/1701): Fix process kill dialog occasionally causing panics.\n- [#1755](https://github.com/ClementTsang/bottom/pull/1755): Fix missing stats/incorrect mount name for certain entries\n  in the disk widget.\n- [#1759](https://github.com/ClementTsang/bottom/pull/1759): Fix increment for data tables if the change is greater than\n  the number of entries left.\n\n### Changes\n\n- [#1559](https://github.com/ClementTsang/bottom/pull/1559): Rename `--enable_gpu` to `--disable_gpu`, and make GPU\n  features enabled by default.\n- [#1570](https://github.com/ClementTsang/bottom/pull/1570): Consider `$XDG_CONFIG_HOME` on macOS when looking for a\n  default config path in a backwards-compatible fashion.\n- [#1686](https://github.com/ClementTsang/bottom/pull/1686): Allow hyphenated arguments to work as well (e.g.\n  `--autohide-time`).\n- [#1701](https://github.com/ClementTsang/bottom/pull/1701): Redesign process kill dialog.\n- [#1706](https://github.com/ClementTsang/bottom/pull/1706): Disable mouse capture when `disable_click` is set.\n- [#1769](https://github.com/ClementTsang/bottom/pull/1769): Change how we calculate swap usage in Windows.\n\n### Other\n\n- [#1655](https://github.com/ClementTsang/bottom/pull/1655): Better handle NVIDIA GPUs on Linux with only\n  libnvidia-ml.so.1.\n- [#1658](https://github.com/ClementTsang/bottom/pull/1658): Make it possible to override completion/manpage generation\n  output directory via env.\n- [#1663](https://github.com/ClementTsang/bottom/pull/1663): Rework how data is stored internally, reducing memory usage\n  a bit.\n- [#1749](https://github.com/ClementTsang/bottom/pull/1749): Fix invalid desktop file values.\n\n## [0.10.2] - 2024-08-05\n\n### Features\n\n- [#1487](https://github.com/ClementTsang/bottom/pull/1487): Add option to move the AVG CPU bar to another row in basic\n  mode.\n\n### Bug Fixes\n\n- [#1541](https://github.com/ClementTsang/bottom/pull/1541): Fix some process details not updating for macOS and\n  Windows.\n- [#1542](https://github.com/ClementTsang/bottom/pull/1542): Fix confusing process run times being reported on macOS.\n- [#1543](https://github.com/ClementTsang/bottom/pull/1543): Fix the `--default_cpu_entry` argument not being checked.\n\n## [0.10.1] - 2024-08-01\n\n### Bug Fixes\n\n- [#1526](https://github.com/ClementTsang/bottom/pull/1526): Fix `--help` description being incorrectly set for a flag,\n  breaking the output.\n\n## [0.10.0] - 2024-08-01\n\n### Features\n\n- [#1276](https://github.com/ClementTsang/bottom/pull/1276): Add GPU process info.\n- [#1353](https://github.com/ClementTsang/bottom/pull/1353): Support selecting the average CPU graph as a default.\n- [#1373](https://github.com/ClementTsang/bottom/pull/1373): Add support for bcachefs in disk widget.\n- [#1430](https://github.com/ClementTsang/bottom/pull/1430): Support controlling the graph legend position for memory\n  and network graph widgets.\n- [#1512](https://github.com/ClementTsang/bottom/pull/1512): Support bold text styling options.\n- [#1514](https://github.com/ClementTsang/bottom/pull/1514): Support italic text styling options.\n\n### Changes\n\n- [#1276](https://github.com/ClementTsang/bottom/pull/1276): NVIDIA GPU functionality is now tied behind the\n  `--enable_gpu` flag. This will likely be changed in the future.\n- [#1344](https://github.com/ClementTsang/bottom/pull/1344): Change the `group` command line-argument to\n  `group_processes` for consistency with the config file option.\n- [#1376](https://github.com/ClementTsang/bottom/pull/1376): Group together related command-line arguments in `-h` and\n  `--help`.\n- [#1411](https://github.com/ClementTsang/bottom/pull/1411): Add `time` as a default column.\n- [#1436](https://github.com/ClementTsang/bottom/pull/1436): Use actual \"swap\" value for Windows.\n- [#1441](https://github.com/ClementTsang/bottom/pull/1441): The following arguments have changed names:\n  - `--left_legend/-l` is now `--cpu_left_legend`.\n- [#1441](https://github.com/ClementTsang/bottom/pull/1441): The following config fields have changed names:\n  - `expanded_on_startup` is now `expanded`.\n  - `left_legend` is now `cpu_left_legend`.\n- [#1458](https://github.com/ClementTsang/bottom/pull/1458): Fix a bug with `--hide_table_gap` with the battery widget.\n- [#1472](https://github.com/ClementTsang/bottom/pull/1472): The following arguments have changed names:\n  - `--mem_as_value` is now `process_memory_as_value`.\n- [#1472](https://github.com/ClementTsang/bottom/pull/1472): The following config fields have changed names:\n  - `mem_as_value` is now `process_memory_as_value`.\n- [#1481](https://github.com/ClementTsang/bottom/pull/1481): The following config fields have changed names:\n  - `disk_filter` is now `disk.name_filter`.\n  - `mount_filter` is now `disk.mount_filter`.\n  - `temp_filter` is now `temperature.sensor_filter`\n  - `net_filter` is now `network.interface_filter`\n- [#1499](https://github.com/ClementTsang/bottom/pull/1499): Redesign how styling is configured.\n- [#1499](https://github.com/ClementTsang/bottom/pull/1499): The following arguments have changed names:\n  - `--colors` is now `--theme`\n- [#1513](https://github.com/ClementTsang/bottom/pull/1513): Table headers are now bold by default.\n- [#1515](https://github.com/ClementTsang/bottom/pull/1515): Show the config path in the error message if unable to\n  read/create a config.\n- [#1682](https://github.com/ClementTsang/bottom/pull/1682): On Linux, temperature sensor labels now always have their\n  first letter capitalized (e.g. \"k10temp: tctl\" -> \"k10temp: Tctl\").\n\n### Bug Fixes\n\n- [#1314](https://github.com/ClementTsang/bottom/pull/1314): Fix fat32 mounts not showing up in macOS.\n- [#1355](https://github.com/ClementTsang/bottom/pull/1355): Reduce chances of non-D0 devices waking up due to\n  temperature checks on Linux.\n- [#1410](https://github.com/ClementTsang/bottom/pull/1410): Fix uptime calculation for Linux.\n\n### Other\n\n- [#1394](https://github.com/ClementTsang/bottom/pull/1394): Add JSON Schema support.\n\n## [0.9.7] - 2024-07-26\n\n## Bug Fixes\n\n- [#1500](https://github.com/ClementTsang/bottom/issues/1500): Fix builds for Rust 1.80.\n\n## [0.9.6] - 2023-08-26\n\n### Other\n\n- [#1286](https://github.com/ClementTsang/bottom/pull/1286): Pin serde to 1.0.188 to help with potential `cargo install`\n  issues. Note this version should be fine and not pull in binaries.\n\n## [0.9.5] - 2023-08-26\n\n### Other\n\n- [#1278](https://github.com/ClementTsang/bottom/pull/1278): Pin serde to 1.0.171.\n\n## [0.9.4] - 2023-08-05\n\n### Features\n\n- [#1248](https://github.com/ClementTsang/bottom/pull/1248): Add I/O counters from ZFS for Linux and FreeBSD.\n\n### Changes\n\n- [#1236](https://github.com/ClementTsang/bottom/pull/1236): Hide the battery tab selector if there is only one battery\n  detected.\n- [#1251](https://github.com/ClementTsang/bottom/pull/1251): Make the charge meter take the entire width of the battery\n  widget.\n\n### Bug Fixes\n\n- [#1230](https://github.com/ClementTsang/bottom/pull/1230): Fix core dump if the terminal is closed while bottom is\n  open.\n- [#1245](https://github.com/ClementTsang/bottom/pull/1245): Fix killing processes in Windows leaving a handle open.\n- [#1264](https://github.com/ClementTsang/bottom/pull/1264): Fix ARC usage showing max system memory instead of max ARC\n  size.\n\n## [0.9.3] - 2023-06-25\n\n### Features\n\n- [#1221](https://github.com/ClementTsang/bottom/pull/1221): Support human times for `rate`.\n\n### Bug Fixes\n\n- [#1216](https://github.com/ClementTsang/bottom/pull/1216): Fix arguments not being sorted alphabetically.\n- [#1219](https://github.com/ClementTsang/bottom/pull/1219): Fix overflow/underflow in graph timespan zoom.\n\n### Other\n\n- [#1206](https://github.com/ClementTsang/bottom/pull/1206): Add `.rpm` package generation.\n- [#1220](https://github.com/ClementTsang/bottom/pull/1220): Update documentation for features supporting human times.\n\n## [0.9.2] - 2023-06-11\n\n### Features\n\n- [#1172](https://github.com/ClementTsang/bottom/pull/1172): Support human times for `time_delta` and\n  `default_time_value`.\n- [#1187](https://github.com/ClementTsang/bottom/pull/1187): Use better names for duplicate temp sensors found by\n  `/sys/class/thermal`.\n- [#1188](https://github.com/ClementTsang/bottom/pull/1188): Also check `/sys/devices/platform/coretemp.*` for temp\n  sensors.\n\n### Bug Fixes\n\n- [#1186](https://github.com/ClementTsang/bottom/pull/1186): Fix for temperature sensor data gathering on Linux\n  immediately halting if any method failed.\n- [#1191](https://github.com/ClementTsang/bottom/pull/1191): Fix ntfs3 mounts not being counted as a physical drive\n  type.\n- [#1195](https://github.com/ClementTsang/bottom/pull/1195): Fix battery health being incorrectly reported on M1 macOS.\n- [#1188](https://github.com/ClementTsang/bottom/pull/1188): Don't fail fast with temperature sensor name generation on\n  Linux.\n\n### Other\n\n- [#1199](https://github.com/ClementTsang/bottom/pull/1199): bottom should build on `aarch64-linux-android` with\n  features disabled.\n\n## [0.9.1] - 2023-05-14\n\n### Bug Fixes\n\n- [#1148](https://github.com/ClementTsang/bottom/pull/1148): Fix Gruvbox colour string being invalid when cache usage is\n  enabled.\n\n## [0.9.0] - 2023-05-10\n\n### Features\n\n- [#1016](https://github.com/ClementTsang/bottom/pull/1016): Add support for displaying process usernames on Windows.\n- [#1022](https://github.com/ClementTsang/bottom/pull/1022): Support three-character hex colour strings for styling.\n- [#1024](https://github.com/ClementTsang/bottom/pull/1024): Support FreeBSD temperature sensors based on\n  `hw.temperature`.\n- [#1063](https://github.com/ClementTsang/bottom/pull/1063): Add buffer and cache memory tracking.\n- [#1106](https://github.com/ClementTsang/bottom/pull/1106): Add current battery charging state.\n- [#1115](https://github.com/ClementTsang/bottom/pull/1115): Add customizable process columns to config file.\n- [#801](https://github.com/ClementTsang/bottom/pull/801): Add optional process time column and querying.\n\n### Changes\n\n- [#1025](https://github.com/ClementTsang/bottom/pull/1025): Officially support M1 macOS.\n- [#1035](https://github.com/ClementTsang/bottom/pull/1035): Migrate away from heim for CPU information.\n- [#1036](https://github.com/ClementTsang/bottom/pull/1036): Migrate away from heim for memory information; bottom will\n  now try to use `MemAvailable` on Linux to determine used memory.\n- [#1041](https://github.com/ClementTsang/bottom/pull/1041): Migrate away from heim for network information.\n- [#1064](https://github.com/ClementTsang/bottom/pull/1064): Migrate away from heim for storage information.\n- [#812](https://github.com/ClementTsang/bottom/issues/812): Fully remove heim from bottom.\n- [#1075](https://github.com/ClementTsang/bottom/issues/1075): Update how drives are named in Windows.\n- [#1106](https://github.com/ClementTsang/bottom/pull/1106): Rename battery consumption field to rate.\n\n### Bug Fixes\n\n- [#1021](https://github.com/ClementTsang/bottom/pull/1021): Fix selected text background colour being wrong if only the\n  foreground colour was set.\n- [#1037](https://github.com/ClementTsang/bottom/pull/1037): Fix `is_list_ignored` accepting all results if set to\n  `false`.\n- [#1064](https://github.com/ClementTsang/bottom/pull/1064): Disk name/mount filter now doesn't always show all entries\n  if one filter wasn't set.\n- [#1064](https://github.com/ClementTsang/bottom/pull/1064): macOS disk I/O is potentially working now.\n- [#597](https://github.com/ClementTsang/bottom/issues/597): Resolve RUSTSEC-2021-0119 by removing heim.\n\n### Other\n\n- [#1100](https://github.com/ClementTsang/bottom/pull/1100): Speed up first draw and first data collection.\n- [#1107](https://github.com/ClementTsang/bottom/pull/1107): Update to clap v4.\n- [#1111](https://github.com/ClementTsang/bottom/pull/1111): Update to\n  regex [1.8.0](https://github.com/rust-lang/regex/blob/93316a3b1adc43cc12fab6c73a59f646658cd984/CHANGELOG.md#180-2023-04-20),\n  supporting more escapable characters and named captures.\n\n## [0.8.0] - 2023-01-22\n\n### Features\n\n- [#950](https://github.com/ClementTsang/bottom/pull/950): Split usage into both usage percentage and usage value.\n\n### Changes\n\n- [#974](https://github.com/ClementTsang/bottom/pull/974): Hide battery duration section if the value is unknown. Also\n  update shortened text.\n- [#975](https://github.com/ClementTsang/bottom/pull/975): Automatically hide the battery widget if no batteries are\n  found but `--battery` is enabled.\n\n### Bug Fixes\n\n- [#950](https://github.com/ClementTsang/bottom/pull/950): Update help menu for disk and temperature widgets with\n  sorting support.\n- [#994](https://github.com/ClementTsang/bottom/pull/994): Fix time graph labels not being styled.\n\n### Other\n\n- [#969](https://github.com/ClementTsang/bottom/pull/969): Follow Debian conventions for naming generated `.deb`\n  binaries.\n\n## [0.7.1] - 2023-01-06\n\n### Bug Fixes\n\n- [#950](https://github.com/ClementTsang/bottom/pull/950): Fix invalid sorting order for disk usage percentage.\n- [#952](https://github.com/ClementTsang/bottom/pull/952), [#960](https://github.com/ClementTsang/bottom/pull/960):\n  Partially fix battery text getting cut off in small windows.\n- [#953](https://github.com/ClementTsang/bottom/pull/953): Fix CPU widget's 'all' label being missing on small sizes.\n\n### Other\n\n- [#951](https://github.com/ClementTsang/bottom/pull/951): Nightly builds now have their version number (`btm -V`)\n  tagged with the commit hash.\n\n## [0.7.0] - 2022-12-31\n\n### Features\n\n- [#676](https://github.com/ClementTsang/bottom/pull/676): Add support for NVIDIA GPU temperature sensors.\n- [#760](https://github.com/ClementTsang/bottom/pull/760): Add a check for whether bottom is being run in a terminal.\n- [#766](https://github.com/ClementTsang/bottom/pull/766): Add FreeBSD support.\n- [#774](https://github.com/ClementTsang/bottom/pull/774): Add half page scrolling with `ctrl-u` and `ctrl-d`.\n- [#784](https://github.com/ClementTsang/bottom/pull/784): Add ZFS ARC support.\n- [#794](https://github.com/ClementTsang/bottom/pull/794): Add GPU memory support for NVIDIA GPUs.\n- [#806](https://github.com/ClementTsang/bottom/pull/806): Update sysinfo to support M1 macOS temperature sensors.\n- [#836](https://github.com/ClementTsang/bottom/pull/836): Add CLI options for GPU memory.\n- [#841](https://github.com/ClementTsang/bottom/pull/841): Add page up/page down support for the help screen.\n- [#868](https://github.com/ClementTsang/bottom/pull/868): Make temperature widget sortable.\n- [#870](https://github.com/ClementTsang/bottom/pull/870): Make disk widget sortable.\n- [#881](https://github.com/ClementTsang/bottom/pull/881): Add pasting to the search bar.\n- [#892](https://github.com/ClementTsang/bottom/pull/892): Add custom retention periods for data.\n- [#899](https://github.com/ClementTsang/bottom/pull/899), [#910](https://github.com/ClementTsang/bottom/pull/910), [#912](https://github.com/ClementTsang/bottom/pull/912):\n  Add non-normalized CPU usage to processes.\n- [#919](https://github.com/ClementTsang/bottom/pull/919): Add an option to expand the default widget on startup.\n\n### Changes\n\n- [#690](https://github.com/ClementTsang/bottom/pull/690): Add some colour to `-h`/`--help` as part of updating to clap\n  3.0.\n- [#726](https://github.com/ClementTsang/bottom/pull/726): Add ARM musl binary build tasks.\n- [#807](https://github.com/ClementTsang/bottom/pull/807): Add more human friendly temperature sensor names for Linux.\n- [#845](https://github.com/ClementTsang/bottom/pull/845), [#922](https://github.com/ClementTsang/bottom/pull/922): Add\n  macOS M1, FreeBSD 12, and FreeBSD 13 binary build tasks.\n- [#916](https://github.com/ClementTsang/bottom/pull/916), [#937](https://github.com/ClementTsang/bottom/pull/937):\n  Improve CPU usage by optimizing draw logic of charts and tables.\n\n### Bug Fixes\n\n- [#711](https://github.com/ClementTsang/bottom/pull/711): Fix building in Rust beta 1.61 due to `as_ref()` calls\n  causing type inference issues.\n- [#717](https://github.com/ClementTsang/bottom/pull/717): Fix clicking on empty space in tables selecting the very last\n  entry of a list in some cases.\n- [#720](https://github.com/ClementTsang/bottom/pull/720): Fix panic if battery feature was disabled during compilation.\n- [#805](https://github.com/ClementTsang/bottom/pull/805): Fix bottom keeping devices awake in certain scenarios.\n- [#825](https://github.com/ClementTsang/bottom/pull/825): Use alternative method of getting parent PID in some cases on\n  macOS devices to avoid needing root access.\n- [#916](https://github.com/ClementTsang/bottom/pull/916): Fix possible gaps with widget layout spacing.\n- [#938](https://github.com/ClementTsang/bottom/pull/938): Fix search scrolling with wider Unicode characters.\n\n## [0.6.8] - 2022-02-01\n\n### Bug Fixes\n\n- [#655](https://github.com/ClementTsang/bottom/pull/669): Fix a bug where the number of CPUs is never refreshed.\n\n## [0.6.7] - 2022-01-31\n\n### Features\n\n- [#646](https://github.com/ClementTsang/bottom/pull/646): Add `PgUp`/`PgDown` keybind support to scroll up and down a\n  page in a table.\n\n### Bug Fixes\n\n- [#655](https://github.com/ClementTsang/bottom/pull/665): Fix bug where the program would stall in an infinite loop if\n  the width of the terminal was too small.\n\n### Other\n\n- [#658](https://github.com/ClementTsang/bottom/pull/658): Update sysinfo.\n\n## [0.6.6] - 2021-12-22\n\n### Changes\n\n- [#637](https://github.com/ClementTsang/bottom/pull/637): Remove duplicate guest time in process CPU calculation\n\n### Bug Fixes\n\n- [#637](https://github.com/ClementTsang/bottom/pull/637): Fix process CPU calculation if /proc/stat CPU line has fewer\n  values than expected\n\n## [0.6.5] - 2021-12-19\n\n### Bug Fixes\n\n- [#600](https://github.com/ClementTsang/bottom/pull/600): Address RUSTSEC-2020-0071\n- [#627](https://github.com/ClementTsang/bottom/pull/627): Fix `process_command` breaking process widget sorting.\n\n### Internal Changes\n\n- [#608](https://github.com/ClementTsang/bottom/pull/608): Add codecov integration to pipeline.\n\n## [0.6.4] - 2021-09-12\n\n### Changes\n\n- [#557](https://github.com/ClementTsang/bottom/pull/557): Add '/s' to network usage legend to better indicate that it's\n  a per-second change.\n\n### Bug Fixes\n\n- [#575](https://github.com/ClementTsang/bottom/pull/575): Updates the procfs library to not crash on kernel version >\n    255.\n\n### Internal Changes\n\n- [#551](https://github.com/ClementTsang/bottom/pull/551): Disable AUR package generation in release pipeline since it's\n  now in community.\n- [#570](https://github.com/ClementTsang/bottom/pull/570): Make battery features optional in compilation.\n\n## [0.6.3] - 2021-07-18\n\n### Changes\n\n- [#547](https://github.com/ClementTsang/bottom/pull/547): Switch Linux memory usage calculation to match htop.\n\n### Bug Fixes\n\n- [#536](https://github.com/ClementTsang/bottom/pull/536): Prevent tests from creating a config file.\n\n- [#542](https://github.com/ClementTsang/bottom/pull/542): Fix missing config options in the default generated config\n  file.\n\n- [#545](https://github.com/ClementTsang/bottom/pull/545): Fix inaccurate memory usage/totals in macOS and Linux, switch\n  unit to binary prefix.\n\n## [0.6.2] - 2021-06-26\n\n### Features\n\n- [#518](https://github.com/ClementTsang/bottom/pull/518): Add `F9` key as an alternative process kill key.\n\n### Bug Fixes\n\n- [#504](https://github.com/ClementTsang/bottom/pull/504): Fix two bugs causing the battery widget colours and mouse\n  events to be broken.\n\n- [#525](https://github.com/ClementTsang/bottom/pull/525): Fix Windows process CPU usage not being divided by the number\n  of cores.\n\n### Internal Changes\n\n- [#506](https://github.com/ClementTsang/bottom/pull/506): Migrate a large portion of documentation over to mkdocs.\n\n## [0.6.1] - 2021-05-11\n\n### Bug Fixes\n\n- [#473](https://github.com/ClementTsang/bottom/pull/473): Fix missing string creation for memory usage in collapsed\n  entries.\n\n## [0.6.0] - 2021-05-09\n\n### Features\n\n- [#263](https://github.com/ClementTsang/bottom/pull/263): Add the option for fine-grained kill signals on Unix-like\n  systems.\n\n- [#333](https://github.com/ClementTsang/bottom/pull/333): Add an \"out of\" indicator that can be enabled using\n  `--show_table_scroll_position` (and its corresponding config option) to help keep track of scrolled position.\n\n- [#379](https://github.com/ClementTsang/bottom/pull/379): Add `--process_command` flag and corresponding config option\n  to default to showing a process' command.\n\n- [#381](https://github.com/ClementTsang/bottom/pull/381): Add a filter in the config file for network interfaces.\n\n- [#392](https://github.com/ClementTsang/bottom/pull/392): Add CPU load averages (1, 5, 15) for Unix-based systems.\n\n- [#406](https://github.com/ClementTsang/bottom/pull/406): Add the Nord colour scheme, as well as a light variant.\n\n- [#409](https://github.com/ClementTsang/bottom/pull/409): Add `Ctrl-w` and `Ctrl-h` shortcuts in search, to delete a\n  word and delete a character respectively.\n\n- [#413](https://github.com/ClementTsang/bottom/pull/413): Add mouse support for sorting process columns.\n\n- [#425](https://github.com/ClementTsang/bottom/pull/425): Add user into the process widget for Unix-based systems.\n\n- [#437](https://github.com/ClementTsang/bottom/pull/437): Redo dynamic network y-axis, add linear scaling, unit type,\n  and prefix options.\n\n- [#445](https://github.com/ClementTsang/bottom/pull/445): Add collapsing in tree mode sums usage to parent.\n\n### Changes\n\n- [#372](https://github.com/ClementTsang/bottom/pull/372): Hide the SWAP graph and legend in normal mode if SWAP is 0.\n\n- [#390](https://github.com/ClementTsang/bottom/pull/390): macOS shouldn't need elevated privileges to see CPU usage on\n  all processes now.\n\n- [#391](https://github.com/ClementTsang/bottom/pull/391): Show degree symbol on Celsius and Fahrenheit.\n\n- [#418](https://github.com/ClementTsang/bottom/pull/418): Removed automatically jumping to the top of the list for\n  process sort shortcuts. The standard behaviour is to now stay in place.\n\n- [#420](https://github.com/ClementTsang/bottom/pull/420): Updated tui-rs, allowing for prettier looking tables!\n\n- [#437](https://github.com/ClementTsang/bottom/pull/437): Add linear interpolation step in drawing step to pr event\n  missing entries on the right side of charts.\n\n- [#443](https://github.com/ClementTsang/bottom/pull/443): Make process widget consistent with disk widget in using\n  decimal prefixes (kilo, mega, etc.) for writes/reads.\n\n- [#449](https://github.com/ClementTsang/bottom/pull/449): Add decimal place to actual memory usage in process widget\n  for values greater or equal to 1GiB.\n\n- [#450](https://github.com/ClementTsang/bottom/pull/450): Tweak `default-light` colour scheme to look less terrible on\n  white terminals.\n\n- [#451](https://github.com/ClementTsang/bottom/pull/451): Add decimal place to disk values larger than 1GB for total\n  read/write in process widgets, and read/write per second in process widgets and disk widgets.\n\n- [#455](https://github.com/ClementTsang/bottom/pull/455): Add a mount point filter for the disk widget. Also tweaked\n  how the filter system works - see the PR for details.\n\n### Bug Fixes\n\n- [#416](https://github.com/ClementTsang/bottom/pull/416): Fix grouped vs ungrouped modes in the processes widget having\n  inconsistent spacing.\n\n- [#417](https://github.com/ClementTsang/bottom/pull/417): Fix the sort menu and sort shortcuts not syncing up.\n\n- [#423](https://github.com/ClementTsang/bottom/pull/423): Fix disk encryption causing the disk widget to fail or not\n  properly map I/O statistics.\n\n- [#425](https://github.com/ClementTsang/bottom/pull/425): Fixed a bug allowing grouped mode in tree mode if already in\n  grouped mode.\n\n- [#467](https://github.com/ClementTsang/bottom/pull/467): Switched CPU usage data source to fix a bug on Windows where\n  occasionally CPU usage would be stuck at 0%.\n\n## [0.5.7] - 2021-01-30\n\n### Bug Fixes\n\n- [#373](https://github.com/ClementTsang/bottom/pull/373): Fix incorrect colours being used the CPU widget in basic\n  mode.\n\n- [#386](https://github.com/ClementTsang/bottom/pull/386): Fix `hide_table_gap` not working in the battery widget.\n\n- [#389](https://github.com/ClementTsang/bottom/pull/389): Fix the sorting arrow disappearing in proc widget under some\n  cases.\n\n- [#398](https://github.com/ClementTsang/bottom/pull/398): Fix basic mode failing to report CPUs if there are less than\n  4 entries to report.\n\n## [0.5.6] - 2020-12-17\n\n### Bug Fixes\n\n- [#361](https://github.com/ClementTsang/bottom/pull/361): Fix temperature sensors not working on non-Linux platforms.\n\n## [0.5.5] - 2020-12-14\n\n### Bug Fixes\n\n- [#349](https://github.com/ClementTsang/bottom/pull/349): Fix CPU graph colours not matching the legend in the \"all\"\n  state.\n\n## [0.5.4] - 2020-12-10\n\n### Changes\n\n- [#344](https://github.com/ClementTsang/bottom/pull/344): Removed the `--debug` option for now.\n\n### Bug Fixes\n\n- [#344](https://github.com/ClementTsang/bottom/pull/344): Fix a performance regression causing high memory and CPU\n  usage over time.\n\n- [#345](https://github.com/ClementTsang/bottom/pull/345): Fix process states not showing.\n\n## [0.5.3] - 2020-11-26\n\n### Bug Fixes\n\n- [#331](https://github.com/ClementTsang/bottom/pull/331): Fix custom battery colour levels being inverted.\n\n## [0.5.2] - 2020-11-25\n\n### Bug Fixes\n\n- [#327](https://github.com/ClementTsang/bottom/pull/327): Fix `hide_avg_cpu` being inverted in config files.\n\n## [0.5.1] - 2020-11-22\n\n### Bug Fixes\n\n- [6ef1d66](https://github.com/ClementTsang/bottom/commit/6ef1d66b2bca49452572a2cabb87d338dcf56e7b): Remove nord as a\n  valid colour for now.\n\n- [e04ce4f](https://github.com/ClementTsang/bottom/commit/e04ce4fa1b42e99f00cf8825bcd58da43552214e): Fix\n  `--use_old_network_legend`.\n\n- [99d0402](https://github.com/ClementTsang/bottom/commit/99d04029f0ebfc73d36adb06ea58ad68f090017c): Fix config\n  detection for built-in colours.\n\n## [0.5.0] - 2020-11-20\n\n### Features\n\n- [#206](https://github.com/ClementTsang/bottom/pull/206): Adaptive network graphs --- prior to this update, graphs were\n  stuck at a range from 0B to 1GiB. Now, they adjust to your current usage and time span, so if you're using, say, less\n  than a MiB, it will cap at a MiB. If you're using 10GiB, then the graph will reflect that and span to a bit greater\n  than 10GiB.\n\n- [#208](https://github.com/ClementTsang/bottom/pull/208): Mouse support for tables and moving to widgets.\n\n- [#217](https://github.com/ClementTsang/bottom/pull/217): (Kinda) ARM support.\n\n- [#220](https://github.com/ClementTsang/bottom/pull/220): Add ability to hide specific temperature and disk entries via\n  config.\n\n- [#223](https://github.com/ClementTsang/bottom/pull/223): Add tree mode for processes.\n\n  - [#312](https://github.com/ClementTsang/bottom/pull/312): Add a `tree` flag to default to the tree mode.\n\n- [#269](https://github.com/ClementTsang/bottom/pull/269): Add simple indicator for when data updating is frozen.\n\n- [#296](https://github.com/ClementTsang/bottom/pull/296): Built-in colour themes.\n\n- [#309](https://github.com/ClementTsang/bottom/pull/309): Add a `mem_as_value` flag to default displaying process\n  memory as value rather than percentage.\n\n### Changes\n\n- [#213](https://github.com/ClementTsang/bottom/pull/213), [#214](https://github.com/ClementTsang/bottom/pull/214):\n  Updated help descriptions, added auto-complete generation.\n\n- [#296](https://github.com/ClementTsang/bottom/pull/296): Changed how we do battery theming. We now only set high,\n  medium, and low colours, and we deal with the ratios.\n\n### Bug Fixes\n\n- [#211](https://github.com/ClementTsang/bottom/pull/211): Fix a bug where you could move down in the process widget\n  even if the process widget search was closed.\n\n- [#215](https://github.com/ClementTsang/bottom/pull/215): Add labels to Linux temperature values.\n\n- [#224](https://github.com/ClementTsang/bottom/pull/224): Implements sorting by count. It previously did absolutely\n  nothing.\n\n- [#238](https://github.com/ClementTsang/bottom/pull/238): Fix being able to cause an index out-of-bounds by resizing\n  to a smaller terminal _just_ after the program got the terminal size, but right before the terminal started drawing.\n\n- [#238](https://github.com/ClementTsang/bottom/pull/238): Fixed not clearing screen before drawing, which caused issues\n  for some environments.\n\n- [#253](https://github.com/ClementTsang/bottom/pull/253): Fix highlighted entries being stuck in another colour when\n  the widget is not selected.\n\n- [#253](https://github.com/ClementTsang/bottom/pull/253), [#266](https://github.com/ClementTsang/bottom/pull/266):\n  Expanding a widget no longer overrides the widget/dialog title colour.\n\n- [#261](https://github.com/ClementTsang/bottom/pull/261): Fixed process names occasionally showing up as truncated, due\n  to only using `/proc/<PID>/stat` as our data source.\n\n- [#262](https://github.com/ClementTsang/bottom/pull/262): Fixed missing thread termination steps as well as improper\n  polling causing blocking in input thread.\n\n- [#289](https://github.com/ClementTsang/bottom/pull/289): Fixed the CPU basic widget showing incorrect data due to an\n  incorrect offset when displaying the data.\n\n- [#290](https://github.com/ClementTsang/bottom/pull/290): Fixed an incorrect offset affecting the CPU colour when\n  scrolling.\n\n- [#291](https://github.com/ClementTsang/bottom/pull/291): Fixed spacing problems in basic CPU mode.\n\n- [#296](https://github.com/ClementTsang/bottom/pull/296): Fixed an incorrect offset affecting the graph CPU colour\n  mismatching the legend.\n\n- [#296](https://github.com/ClementTsang/bottom/pull/296): Removes an accidental extra comma in one of the headers in\n  the disk widget.\n\n- [#308](https://github.com/ClementTsang/bottom/pull/308): Removes the automatically generated CPU colours method.\n\n## [0.4.7] - 2020-08-26\n\n### Bug Fixes\n\n- [#204](https://github.com/ClementTsang/bottom/pull/204): Fix searching by command name being broken.\n\n## [0.4.6] - 2020-08-25\n\n### Features\n\n- [#179](https://github.com/ClementTsang/bottom/pull/179): Show full command/process path as an option.\n\n- [#183](https://github.com/ClementTsang/bottom/pull/183): Added sorting capabilities to any column.\n\n- [#188](https://github.com/ClementTsang/bottom/pull/188): Add (estimated) memory usage values, toggle this from percent\n  to values for processes with `%`.\n\n- [#196](https://github.com/ClementTsang/bottom/pull/196): Support searching processes by process state.\n\n- Added `WASD` as an alternative widget movement system.\n\n- [#198](https://github.com/ClementTsang/bottom/pull/198): Allow `e` to also escape expanded mode.\n\n### Changes\n\n- [#181](https://github.com/ClementTsang/bottom/pull/181): Changed to just support stable (and newer) Rust, due to\n  library incompatibilities.\n\n- [#182](https://github.com/ClementTsang/bottom/pull/182): For macOS, support `$HOME/Library/Application Support`\n  instead of `$HOME/.config` for config files. For backwards compatibility's sake, for macOS, this will still check\n  `.config` if it exists first, but otherwise, it will default to the new location.\n\n### Bug Fixes\n\n- [#183](https://github.com/ClementTsang/bottom/pull/183): Fixed bug in basic mode where the battery widget was placed\n  incorrectly.\n\n- [#186](https://github.com/ClementTsang/bottom/pull/186): Fixed a bug caused by hitting `Enter` when a process kill\n  fails, breaking future process kills.\n\n- [#187](https://github.com/ClementTsang/bottom/pull/187): Fix bug caused by incorrectly reading the `/proc/{pid}/stats`\n  file.\n\n## [0.4.5] - 2020-07-08\n\n- No changes in this update, just an uptick for Crates.io using the wrong Cargo.lock.\n\n## [0.4.4] - 2020-07-06\n\n### Features\n\n- [#114](https://github.com/ClementTsang/bottom/pull/114): Show process state per process (originally in 0.4.0, moved to\n  later). This only shows if the processes are not merged together; I couldn't think of a nice way to show it when\n  grouped together, unfortunately.\n\n### Changes\n\n- [#156](https://github.com/ClementTsang/bottom/issues/156) - Removal of the `/` CPU core showing in the chart. It felt\n  clunky to use, was not really useful, and hard to work with large core counts.\n\n  Furthermore:\n\n  - `show_disabled_data` option and flag is removed.\n\n  - Average CPU is now on by _default_. You can disable it via `-a, --hide_avg_cpu` or `hide_avg_cpu = true`.\n\n  - Make highlighted CPU persist even if widget is not selected - this should help make it easier to know what CPU you\n      are looking at even if you aren't currently on the CPU widget.\n\n### Bug Fixes\n\n- [#164](https://github.com/ClementTsang/bottom/issues/164) - Fixed a bug where bottom would incorrectly read the wrong\n  values to calculate the read/write columns for processes in Linux.\n\n- [#165](https://github.com/ClementTsang/bottom/issues/165) - Fixed a bug where OR operations in the process query\n  wouldn't properly for some cases.\n\n- The process query should hopefully be a bit more usable now. There were issues with how spaces (which are treated as\n  an AND if it was between keywords, so something like `btm cpu > 0 mem > 0` would look for a process named `btm` with\n  cpu usage > 0 and mem usage > 0). This has been hopefully improved.\n\n## [0.4.3] - 2020-05-15\n\n### Other\n\n- Update sysinfo version that fixes an overflow issue.\n\n## [0.4.2] - 2020-05-11\n\n### Changes\n\n- Automatically hide time axis labels if the widget gets too small.\n\n- Automatically hide table gap if the widget gets too small.\n\n### Bug Fixes\n\n- The `<Space>` character can be used as an \"AND\" again (properly) in queries. For example:\n\n```bash\n(btm cpu > 0) (discord mem > 0)\n```\n\nis equivalent to:\n\n```bash\n(btm AND cpu > 0) AND (discord AND mem > 0)\n```\n\n- [#151](https://github.com/ClementTsang/bottom/issues/151) - Fixed an issue where if the drive I/O label didn't match\n  any disk, the entire disk widget would display nothing.\n\n- Display SWAP and MEM legends even if the total amount is 0 to avoid a weird blank spot in the legend.\n\n## [0.4.1] - 2020-05-05\n\n### Bug Fixes\n\n- [#146](https://github.com/ClementTsang/bottom/pull/146): Fixed a typo in the help menu (credit\n  to [HarHarLinks](https://github.com/HarHarLinks)).\n\n## [0.4.0] - 2020-05-04\n\n### Features\n\n- [#58](https://github.com/ClementTsang/bottom/issues/58): I/O stats per process.\n\n- [#55](https://github.com/ClementTsang/bottom/issues/55): Battery monitoring widget.\n\n- [#134](https://github.com/ClementTsang/bottom/pull/134): `hjkl` movement to delete dialog (credit\n  to [andys8](https://github.com/andys8)).\n\n- [#59](https://github.com/ClementTsang/bottom/issues/59): `Alt-h` and `Alt-l` to move left/right in query (and rest of\n  the app actually).\n\n- [#59](https://github.com/ClementTsang/bottom/issues/59): Added a more advanced querying system.\n\n### Changes\n\n- Changed default colours for highlighted borders and table headers to light blue - this is mostly to deal with\n  Powershell colour conflicts.\n\n- Updated the widget type keyword list to accept the following keywords as existing types:\n\n  - `\"memory\"`\n  - `\"network\"`\n  - `\"process\"`\n  - `\"processes\"`\n  - `\"temperature\"`\n\n- [#117](https://github.com/ClementTsang/bottom/issues/117): Update tui to 0.9:\n\n  - Removed an (undocumented) feature in allowing modifying total RX/TX colours. This is mainly due to the legend\n      change.\n\n  - Use custom legend-hiding to stop hiding legends for memory and network widgets.\n\n  - In addition, changed to using only legends within the graph for network, as well as redesigned the legend.\n      The old legend style can still be used via the `--use_old_network_legend` flag or `use_old_network_legend = true`\n      config option.\n\n  - Allow for option to hide the header gap on tables via `--hide_table_gap` or `hide_table_gap = true`.\n\n- [#126](https://github.com/ClementTsang/bottom/pull/126): Updated error messages to be a bit more consistent/helpful.\n\n- [#70](https://github.com/ClementTsang/bottom/issues/70): Redesigned help menu to allow for scrolling.\n\n- [#59](https://github.com/ClementTsang/bottom/issues/59): Moved maximization key to `e`, renamed feature to _expanding_\n  the widget. Done to allow for the `<Enter>` key to be used later for a more intuitive usage.\n\n### Bug Fixes\n\n- Fixed `dd` not working on non-first entries.\n\n- Fixed bug where a single empty row as a layout would crash without a proper warning.\n  The behaviour now errors out with a more helpful message.\n\n- Fixed bug where empty widgets in layout would cause widget movement to not work properly when moving vertically.\n\n### Internal changes\n\n- [#38](https://github.com/ClementTsang/bottom/issues/38): Updated arg tests and added config testing.\n\n- Add MSRV, starting with 1.40.0.\n\n## [0.3.0] - 2020-04-07\n\n### Features\n\n- [#20](https://github.com/ClementTsang/bottom/issues/20): Time scaling was added to allow users to zoom in/out based on\n  their desired time intervals. Time markers on the charts can be hidden or automatically hidden.\n\n- [#37](https://github.com/ClementTsang/bottom/issues/37): Automatically populate a config file if one does not exist.\n\n- [#21](https://github.com/ClementTsang/bottom/issues/21): Basic mode added.\n\n- [#51](https://github.com/ClementTsang/bottom/issues/51): Modularity with widget placement or inclusion added.\n\n### Changes\n\n- Removed redundant dependencies.\n\n- [#17](https://github.com/ClementTsang/bottom/issues/17): Add colouring options to the total RX/TX labels.\n\n- [#29](https://github.com/ClementTsang/bottom/issues/29): Added `F1-F3` keys as alternatives for selecting search\n  options\n\n- [#42](https://github.com/ClementTsang/bottom/issues/42), [#45](https://github.com/ClementTsang/bottom/issues/45), [#35](https://github.com/ClementTsang/bottom/issues/35):\n  Change the arrow used for sorting processes to work with other terminals.\n\n- [#61](https://github.com/ClementTsang/bottom/issues/61): Search box changed to not block if the window is small.\n\n- [#40](https://github.com/ClementTsang/bottom/issues/40): Rewrote README to be more clear and explicit.\n\n- [#109](https://github.com/ClementTsang/bottom/issues/109): Sorting processes by name is case-insensitive.\n\n### Bug Fixes\n\n- [#33](https://github.com/ClementTsang/bottom/issues/33): Fix bug with search and graphemes bigger than a byte crashing\n  due to the cursor.\n\n- [#41](https://github.com/ClementTsang/bottom/issues/41): Fix bug that caused the cursor to go off-screen while\n  searching.\n\n- [#61](https://github.com/ClementTsang/bottom/issues/61): Dialog boxes set to be a constant width/height.\n\n- [#80](https://github.com/ClementTsang/bottom/issues/80): Fix bug with resizing and scrolling causing issues with\n  tables.\n\n- [#77](https://github.com/ClementTsang/bottom/issues/77): Fixed hidden CPU entries from being scrolled to.\n\n- [#79](https://github.com/ClementTsang/bottom/issues/79): Fixed CPU entries being a different colour if the one above\n  it was hidden.\n\n- [#85](https://github.com/ClementTsang/bottom/pull/85): A div-by-zero error when the memory values were zero was fixed.\n\n### Other\n\n- Various Travis changes.\n\n- Scoop install option added.\n\n## [0.2.2] - 2020-02-26\n\n### Features\n\n- Added support for colouring the average CPU core separately in config files.\n\n- [#15](https://github.com/ClementTsang/bottom/issues/15) - Added support for (some) named colours and RGB values in\n  config files.\n\n### Bug Fixes\n\n- [#28](https://github.com/ClementTsang/bottom/issues/30): Fixed broken Cargo.toml for Cargo installs.\n\n- Fixed Windows issue with shift key.\n\n- [#14](https://github.com/ClementTsang/bottom/issues/14): Ignore certain characters in search\n\n## [0.2.1] - 2020-02-21\n\n### Bug Fixes\n\n- [#14](https://github.com/ClementTsang/bottom/issues/11): Fixed default config paths not being read properly.\n\n## [0.2.0] - 2020-02-20\n\n### Features\n\n- Searching in processes was added.\n\n- The option of a config file was added. Config files follow the TOML spec. These support boot flags by default, and\n  colour schemes.\n\n- The capability of maximizing a widget to take up all draw space was added.\n\n- Filtering out CPU cores on the graph/legend was added.\n\n### Changes\n\n- Default colours were changed for better support on macOS Terminal and PowerShell.\n\n- Rewrote and refactored how I get data to be less spaghetti. This might also have the added benefit of running better,\n  with less duplicated logic.\n\n- Changed how the dd dialog and help dialog look. Hopefully they'll be nicer to look at and more intuitive to use!\n\n### Bug Fixes\n\n- [#2](https://github.com/ClementTsang/bottom/issues/2): Fixed issues where the program would crash if the window was\n  too small.\n\n- Added a panic handler so terminals won't get all broken if a panic _does_ still occur.\n\n- Fixed some sizing issues, hopefully this means that it's still readable at smaller sizes (within reason).\n\n- [#10](https://github.com/ClementTsang/bottom/issues/10): Fixed scroll issue caused by resizing.\n\n## [0.1.2] - 2020-01-11\n\n### Changes\n\n- Added a bit more complexity to how we determine column widths for tables. This should fix an issue where columns would\n  glitch out at smaller widths, and hopefully look nicer.\n\n### Bug Fixes\n\n- Rewrote scroll logic in tables to avoid some strange scroll behaviour I encountered where it would jump around.\n\n- Attempt to patch a panic caused by the change in how we determine time in the data collection stage.\n\n## [0.1.1] - 2020-01-11\n\n### Features\n\n- `Tab` in the processes widget will now group similarly-named processes together (as well as their total CPU and MEM\n  usage). `dd`-ing this will try to kill all entries with that process name.\n\n- A flag to enable this by default is also now available.\n\n### Bug Fixes\n\n- Accidentally left in a bug in which the disk widget was using megabytes instead of bytes as their unit during data\n  collection... but during data conversion for the display I treated them as bytes.\n\n## [0.1.0] - 2020-01-11\n\nInitial release.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution\n\nContribution in any way is appreciated, whether it is reporting problems, fixing bugs, implementing features, improving the documentation, etc.\n\n## Opening an issue\n\n### Bug reports\n\nWhen filing a bug report, fill out the [bug report template](https://github.com/ClementTsang/bottom/issues/new?assignees=&labels=bug&template=bug_report.yml). Be sure to give all the necessary details! It is _incredibly_ difficult for a maintainer to fix a bug when it cannot be reproduced,\nso that makes it much easier to reproduce the problem!\n\n### Feature requests\n\nPlease fill out the [feature request template](https://github.com/ClementTsang/bottom/issues/new?assignees=&labels=feature&template=feature_request.yml). Remember to give details about what the feature is along with why you think this suggestion will be useful.\n\n## Pull requests\n\nIf you want to directly contribute documentation changes or code, follow this! The expected workflow for a pull request is:\n\n1. Fork the project.\n2. Make your changes.\n3. Make any documentation changes if necessary - if you add a new feature, it'll probably need documentation changes. See [here](https://bottom.pages.dev/nightly/contribution/documentation/) for tips on documentation.\n4. Commit and create a pull request to merge into the `main` branch. **Please fill out the pull request template**.\n5. Ask a maintainer to review your pull request.\n   - Check if the CI workflow passes. These consist of clippy lints, rustfmt checks, and basic tests. If you are a\n     first-time contributor, you may need to wait for a maintainer to let CI run.\n   - If changes are suggested or any comments are made, they should probably be addressed.\n6. Once it looks good, it'll be merged! Note that _generally_, PRs are squashed to maintain repo cleanliness, though\n   feel free to ask otherwise if that isn't preferable.\n\nFor more details, see [here](https://bottom.pages.dev/nightly/contribution/issues-and-pull-requests/).\n\n### Documentation\n\nFor contributing to documentation, see [here](https://bottom.pages.dev/nightly/contribution/documentation/).\n\n### Packaging\n\nIf you want to become a package maintainer, see [here](https://bottom.pages.dev/nightly/contribution/packaging-and-distribution/)\nfor details on how to build bottom, how to generate/obtain completion files and manpages, and how to add installation instructions for the package to the README.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"bottom\"\nversion = \"0.12.3\"\nrepository = \"https://github.com/ClementTsang/bottom\"\nlicense = \"MIT\"\ndescription = \"A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows.\"\ndocumentation = \"https://bottom.pages.dev/stable\"\nreadme = \"README.md\"\ndefault-run = \"btm\"\nbuild = \"build.rs\"\nauthors = [\"Clement Tsang <cjhtsang@uwaterloo.ca>\"]\nkeywords = [\"cross-platform\", \"monitoring\", \"cli\", \"top\", \"tui\"]\ncategories = [\"command-line-utilities\", \"visualization\"]\nexclude = [\n    \".cargo-husky/\",\n    \".github/\",\n    \".idea/\",\n    \".vscode/\",\n    \"assets/\",\n    \"desktop/\",\n    \"docs/\",\n    \"flamegraphs/\",\n    \"sample_configs/\",\n    \"schema/\",\n    \"scripts/\",\n    \"wix/\",\n    \".all-contributorsrc\",\n    \".cirrus.yml\",\n    \".gitignore\",\n    \".markdownlint.json\",\n    \"CHANGELOG.md\",\n    \"clippy.toml\",\n    \"codecov.yml\",\n    \"CONTRIBUTING.md\",\n    \"Cross.toml\",\n    \"debug.log\",\n    \"flamegraph.svg\",\n    \"profile.json.gz\",\n    \"rustfmt.toml\",\n]\nedition = \"2024\"\n# The oldest version I've tested that should still build - note this is not an official MSRV!\nrust-version = \"1.85\"\n\n[lib]\ntest = true\ndoctest = true\ndoc = true\n\n[[bin]]\nname = \"btm\"\npath = \"src/bin/main.rs\"\ndoc = false\n\n[[bin]]\nname = \"schema\"\npath = \"src/bin/schema.rs\"\ntest = false\ndoctest = false\ndoc = false\nrequired-features = [\"generate_schema\"]\n\n[features]\n# Used for general builds.\nbattery = [\"starship-battery\"]\nnvidia = [\"nvml-wrapper\"]\ngpu = [\"nvidia\"]\nzfs = []\ndeploy = [\"battery\", \"gpu\", \"zfs\"]\ndefault = [\"deploy\"]\n\n# Should not be included in builds.\nlogging = [\"fern\", \"log\", \"time\"]\ngenerate_schema = [\"schemars\", \"serde_json\", \"strum\"]\n\n[dependencies]\nanyhow = \"1.0.101\"\ncfg-if = \"1.0.4\"\nclap = { version = \"4.5.57\", features = [\"default\", \"cargo\", \"wrap_help\", \"derive\"] }\nconcat-string = \"1.0.1\"\ncrossterm = \"0.29.0\"\nctrlc = { version = \"3.5.0\", features = [\"termination\"] }\ndirs = \"6.0.0\"\nhumantime = \"2.3.0\"\nindexmap = \"2.13.0\"\nindoc = \"2.0.7\"\nitertools = \"0.14.0\"\nnohash = \"0.2.0\"\nnvml-wrapper = { version = \"0.11.0\", optional = true, features = [\"legacy-functions\"] }\nratatui-core = \"0.1.0\"\nregex = \"1.12.3\"\nrustc-hash = \"2.1.1\"\nserde = { version = \"1.0.228\", features = [\"derive\"] }\nstarship-battery = { version = \"0.10.3\", optional = true }\nsysinfo = \"=0.38.0\"\ntimeless = \"0.0.14-alpha\"\ntoml_edit = { version = \"0.24.0\", features = [\"serde\"] }\ntui = { version = \"0.30.0\", package = \"ratatui\", features = [\n    \"unstable-rendered-line-info\",\n    \"layout-cache\",\n    \"crossterm\",\n] }\nunicode-ellipsis = \"0.3.0\"\nunicode-segmentation = \"1.12.0\"\nunicode-width = \"0.2.2\"\n\n# Used for logging. Mostly a debugging tool.\nfern = { version = \"0.7.1\", optional = true }\nlog = { version = \"0.4.29\", optional = true }\ntime = { version = \"0.3.47\", features = [\"local-offset\", \"formatting\", \"macros\"], optional = true }\n\n# These are just used for JSON schema generation.\nschemars = { version = \"1.2.1\", optional = true }\nserde_json = { version = \"1.0.149\", optional = true }\nstrum = { version = \"0.27.2\", features = [\"derive\"], optional = true }\n\n[target.'cfg(unix)'.dependencies]\nlibc = \"0.2.180\"\n\n[target.'cfg(target_os = \"linux\")'.dependencies]\nrustix = { version = \"1.1.2\", features = [\"fs\", \"param\"] }\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\ncore-foundation = \"0.10.1\"\nmach2 = \"0.6.0\"\n\n[target.'cfg(target_os = \"windows\")'.dependencies]\nwindows = { version = \"0.62.2\", features = [\n    \"Win32_Foundation\",\n    \"Win32_Security\",\n    \"Win32_Storage_FileSystem\",\n    \"Win32_System_IO\",\n    \"Win32_System_Ioctl\",\n    \"Win32_System_Performance\",\n    \"Win32_System_ProcessStatus\",\n    \"Win32_System_Threading\",\n] }\n\n[target.'cfg(target_os = \"freebsd\")'.dependencies]\nserde_json = { version = \"1.0.145\" }\nsysctl = { version = \"0.7.1\" }\nfiledescriptor = \"0.8.3\"\n\n[dev-dependencies]\nassert_cmd = \"2.1.2\"\npredicates = \"3.1.3\"\ntempfile = { version = \"3.23.0\", default-features = false }\n\n[target.'cfg(all(target_arch = \"x86_64\", target_os = \"linux\"))'.dev-dependencies]\nportable-pty = \"0.9.0\"\n\n[build-dependencies]\nclap = { version = \"4.5.57\", features = [\"default\", \"cargo\", \"wrap_help\", \"derive\"] }\nclap_complete = \"4.5.62\"\nclap_complete_nushell = \"4.5.10\"\nclap_complete_fig = \"4.5.2\"\nclap_mangen = \"0.2.31\"\nindoc = \"2.0.7\"\n\n# Compile dependencies with optimizations enabled, even in debug mode.\n[profile.dev.package.\"*\"]\nopt-level = 3\n\n[profile.release]\ndebug = 0\nstrip = \"symbols\"\nlto = true\nopt-level = 3\ncodegen-units = 1\n\n[profile.profiling]\ninherits = \"release\"\ndebug = true\nstrip = false\n\n[package.metadata.deb]\nsection = \"utility\"\nassets = [\n    [\n        \"target/release/btm\",\n        \"usr/bin/\",\n        \"755\",\n    ],\n    [\n        \"LICENSE\",\n        \"usr/share/doc/btm/\",\n        \"644\",\n    ],\n    [\n        \"manpage/btm.1.gz\",\n        \"usr/share/man/man1/btm.1.gz\",\n        \"644\",\n    ],\n    [\n        \"completion/btm.bash\",\n        \"usr/share/bash-completion/completions/btm\",\n        \"644\",\n    ],\n    [\n        \"completion/btm.fish\",\n        \"usr/share/fish/vendor_completions.d/btm.fish\",\n        \"644\",\n    ],\n    [\n        \"completion/_btm\",\n        \"usr/share/zsh/vendor-completions/\",\n        \"644\",\n    ],\n    [\n        \"desktop/bottom.desktop\",\n        \"usr/share/applications/bottom.desktop\",\n        \"644\",\n    ],\n    [\n        \"assets/icons/bottom-system-monitor.svg\",\n        \"/usr/share/icons/hicolor/scalable/apps/bottom-system-monitor.svg\",\n        \"644\",\n    ],\n]\nconflicts = \"btm\"\nextended-description = \"\"\"\n\nBy default, bottom will look for a config file in ~/.config/bottom/bottom.toml. A config file can be specified \\\nusing `-C`. If a config file does not exist at the specified or default location, a default one will be created \\\nfor the user there.\n\"\"\"\n\n[package.metadata.deb.variants.arm64]\ndepends = \"libc6:arm64 (>= 2.28)\"\n\n[package.metadata.deb.variants.armhf]\ndepends = \"libc6:armhf (>= 2.28)\"\n\n[package.metadata.wix]\noutput = \"bottom_installer.msi\"\nproduct-icon = \"assets/icons/bottom.ico\"\n\n[package.metadata.generate-rpm]\nassets = [\n    { source = \"target/release/btm\", dest = \"/usr/bin/\", mode = \"755\" },\n    { source = \"LICENSE\", dest = \"/usr/share/doc/btm/\", mode = \"644\" },\n    { source = \"manpage/btm.1.gz\", dest = \"/usr/share/man/man1/btm.1.gz\", mode = \"644\", doc = true },\n    { source = \"completion/btm.bash\", dest = \"/usr/share/bash-completion/completions/btm\", mode = \"644\" },\n    { source = \"completion/btm.fish\", dest = \"/usr/share/fish/vendor_completions.d/btm.fish\", mode = \"644\" },\n    { source = \"completion/_btm\", dest = \"/usr/share/zsh/vendor-completions/\", mode = \"644\" },\n    { source = \"desktop/bottom.desktop\", dest = \"/usr/share/applications/bottom.desktop\", mode = \"644\" },\n    { source = \"assets/icons/bottom-system-monitor.svg\", dest = \"/usr/share/icons/hicolor/scalable/apps/bottom-system-monitor.svg\", mode = \"644\" },\n]\n\n[lints.rust]\nrust_2018_idioms = \"deny\"\n# missing_docs = \"deny\"\n\n[lints.rustdoc]\nbroken_intra_doc_links = \"deny\"\nprivate_intra_doc_links = \"deny\"\nmissing_crate_level_docs = \"deny\"\n\n[lints.clippy]\ntodo = \"deny\"\nunimplemented = \"deny\"\nmissing_safety_doc = \"deny\"\nunwrap_used = \"deny\"\n"
  },
  {
    "path": "Cross.toml",
    "content": "[build.env]\npassthrough = [\"RUST_BACKTRACE\", \"BTM_GENERATE\"]\n\n[target.x86_64-unknown-netbsd]\npre-build = [\n    \"apt-get update && apt-get install -y --no-install-recommends curl xz-utils && td=$(mktemp -d) && mkdir -p \\\"$td/netbsd\\\" && curl -4 --retry 3 -sSfL 'http://ftp.netbsd.org/pub/NetBSD/NetBSD-9.3/amd64/binary/sets/base.tar.xz' | tar -C \\\"$td/netbsd\\\" -xJ ./usr/lib ./lib && curl -4 --retry 3 -sSfL 'http://ftp.netbsd.org/pub/NetBSD/NetBSD-9.3/amd64/binary/sets/comp.tar.xz' | tar -C \\\"$td/netbsd\\\" -xJ ./usr/lib && cp \\\"$td\\\"/netbsd/usr/lib/libkvm* /usr/local/x86_64-unknown-netbsd/lib/ && rm -rf \\\"$td\\\"\",\n]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Clement Tsang\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <h1>bottom (btm)</h1>\n\n  <p>\n    A customizable cross-platform graphical process/system monitor for the terminal.<br />Supports Linux, macOS, and Windows. Inspired by <a href=https://github.com/aksakalli/gtop>gtop</a>, <a href=https://github.com/xxxserxxx/gotop>gotop</a>, and <a href=https://github.com/htop-dev/htop>htop</a>.\n  </p>\n\n[<img src=\"https://img.shields.io/github/checks-status/ClementTsang/bottom/main?style=flat-square&logo=github\" alt=\"CI status\">](https://github.com/ClementTsang/bottom/actions?query=branch%3Amain)\n[<img src=\"https://img.shields.io/crates/v/bottom.svg?style=flat-square\" alt=\"crates.io link\">](https://crates.io/crates/bottom)\n[<img src=\"https://img.shields.io/badge/docs-stable-66c2a5?style=flat-square&labelColor=555555&logoColor=white\" alt=\"Stable documentation\">](https://bottom.pages.dev/stable)\n[<img src=\"https://img.shields.io/badge/docs-nightly-88c0d0?style=flat-square&labelColor=555555&logoColor=white\" alt=\"Nightly documentation\">](https://bottom.pages.dev/nightly)\n[<img src=\"https://img.shields.io/badge/docs-mirror-8A2BE2?style=flat-square&labelColor=555555&logoColor=white\" alt=\"Doc mirror on GitHub Pages\">](https://clementtsang.github.io/bottom/stable)\n\n</div>\n\n<div align=\"center\">\n  <img src=\"assets/demo.gif\" alt=\"Quick demo recording showing off bottom's searching, expanding, and process killing.\"/>\n  <p>\n    <sub>\n      Demo using the <a href=\"https://github.com/morhetz/gruvbox\">Gruvbox</a> theme (<code>--theme gruvbox</code>), along with <a href=\"https://www.ibm.com/plex/\">IBM Plex Mono</a> and <a href=\"https://sw.kovidgoyal.net/kitty/\">Kitty</a>\n    </sub>\n  </p>\n</div>\n\n## Table of contents <!-- omit in toc -->\n\n- [Features](#features)\n- [Support](#support)\n  - [Official](#official)\n  - [Unofficial](#unofficial)\n- [Installation](#installation)\n  - [Cargo](#cargo)\n  - [Alpine](#alpine)\n  - [Arch Linux](#arch-linux)\n  - [Debian / Ubuntu](#debian--ubuntu)\n  - [Exherbo Linux](#exherbo-linux)\n  - [Fedora / CentOS / AlmaLinux / Rocky Linux](#fedora--centos--almalinux--rocky-linux)\n    - [COPR](#copr)\n    - [Terra](#terra)\n    - [RPM](#rpm)\n  - [Gentoo](#gentoo)\n  - [Nix](#nix)\n  - [openSUSE](#opensuse)\n  - [Snap](#snap)\n  - [Solus](#solus)\n  - [Void](#void)\n  - [gah](#gah)\n  - [Homebrew](#homebrew)\n  - [MacPorts](#macports)\n  - [Chocolatey](#chocolatey)\n  - [Scoop](#scoop)\n  - [winget](#winget)\n  - [Windows installer](#windows-installer)\n  - [Conda](#conda)\n  - [mise](#mise)\n  - [Pre-built binaries](#pre-built-binaries)\n    - [Auto-completion](#auto-completion)\n- [Usage](#usage)\n- [Configuration](#configuration)\n- [Troubleshooting](#troubleshooting)\n- [Documentation](#documentation)\n- [Contribution](#contribution)\n  - [Contributors](#contributors)\n- [Thanks](#thanks)\n\n## Features\n\nAs (yet another) process/system visualization and management application, bottom supports the typical features:\n\n- Graphical visualization widgets for:\n\n  - [CPU usage](https://bottom.pages.dev/nightly/usage/widgets/cpu/) over time, at an average and per-core level\n  - [RAM and swap usage](https://bottom.pages.dev/nightly/usage/widgets/memory/) over time\n  - [Network I/O usage](https://bottom.pages.dev/nightly/usage/widgets/network/) over time\n\n  with support for zooming in/out the current time interval displayed.\n\n- Widgets for displaying info about:\n\n  - [Disk capacity/usage](https://bottom.pages.dev/nightly/usage/widgets/disk/)\n  - [Temperature sensors](https://bottom.pages.dev/nightly/usage/widgets/temperature/)\n  - [Battery usage](https://bottom.pages.dev/nightly/usage/widgets/battery/)\n\n- [A process widget](https://bottom.pages.dev/nightly/usage/widgets/process/) for displaying, sorting, and searching info about processes, as well as support for:\n\n  - [Kill signals](https://bottom.pages.dev/nightly/usage/widgets/process/#process-termination)\n  - [Tree mode](https://bottom.pages.dev/nightly/usage/widgets/process/#tree-mode)\n\n- [Cross-platform support](https://github.com/ClementTsang/bottom#support) for Linux, macOS, and Windows, with more planned in the future.\n\n- [Customizable behaviour](https://bottom.pages.dev/nightly/configuration/command-line-options/) that can be controlled with command-line options or a config file, such as:\n\n  - Custom and built-in colour themes\n  - Customizing widget behaviour\n  - Changing the layout of widgets\n  - Filtering out entries in some widgets\n\n- And more:\n\n  - [An htop-inspired basic mode](https://bottom.pages.dev/nightly/usage/basic-mode/)\n  - [Expansion, which focuses on just one widget](https://bottom.pages.dev/nightly/usage/general-usage/#expansion)\n\n- And more!\n\nYou can find more details in [the documentation](https://bottom.pages.dev/nightly/usage/general-usage/).\n\n## Support\n\n### Official\n\nbottom _officially_ supports the following operating systems and corresponding architectures:\n\n- macOS (`x86_64`, `aarch64`)\n- Linux (`x86_64`, `i686`, `aarch64`)\n- Windows (`x86_64`, `i686`)\n\nThese platforms are tested to work for the most part and issues on these platforms will be fixed if possible.\nFurthermore, binaries are built and tested using the most recent version of stable Rust at the time.\n\nFor more details on supported platforms and known problems, check out [the documentation](https://bottom.pages.dev/nightly/support/official/).\n\n### Unofficial\n\nbottom may work on a number of platforms that aren't officially supported. Note that unsupported platforms:\n\n- Might not be tested in CI to build or pass tests (see [here](./.github/workflows/ci.yml) for checked platforms).\n- Might not be properly tested by maintainers prior to a stable release.\n- May only receive limited support, such as missing features or bugs that may not be fixed.\n\nNote that some unsupported platforms may eventually be officially supported (e.g., FreeBSD).\n\nA non-comprehensive list of some currently unofficially-supported platforms that may compile/work include:\n\n- FreeBSD (`x86_64`)\n- Linux (`armv6`, `armv7`, `powerpc64le`, `riscv64gc`, `loongarch64`)\n- Android (`arm64`)\n- Windows (`arm64`)\n\nFor more details on unsupported platforms and known problems, check out [the documentation](https://bottom.pages.dev/nightly/support/unofficial/).\n\n## Installation\n\n### Cargo\n\nInstallation via `cargo` can be done by installing the [`bottom`](https://crates.io/crates/bottom) crate:\n\n```bash\n# You might need to update the stable version of Rust first.\n# Other versions might work, but this is not guaranteed.\nrustup update stable\n\n# Install the binary from crates.io.\ncargo install bottom --locked\n\n# If you use another channel by default, you can specify\n# the what channel to use like so:\ncargo +stable install bottom --locked\n\n# --locked may be omitted if you wish to not use the\n# locked crate versions in Cargo.lock. However, be\n# aware that this may cause problems with dependencies.\ncargo install bottom\n```\n\nAlternatively, you can use `cargo install` using the repo as the source.\n\n```bash\n# You might need to update the stable version of Rust first.\n# Other versions might work, but this is not guaranteed.\nrustup update stable\n\n# Option 1 - Download an archive from releases and install\ncurl -LO https://github.com/ClementTsang/bottom/archive/0.12.3.tar.gz\ntar -xzvf 0.12.3.tar.gz\ncargo install --path . --locked\n\n# Option 2 - Manually clone the repo and install\ngit clone https://github.com/ClementTsang/bottom\ncd bottom\ncargo install --path . --locked\n\n# Option 3 - Install using cargo with the repo as the source\ncargo install --git https://github.com/ClementTsang/bottom --locked\n\n# You can also pass in the target-cpu=native flag to try to\n# use better CPU-specific optimizations. For example:\nRUSTFLAGS=\"-C target-cpu=native\" cargo install --path . --locked\n```\n\n### Alpine\n\nbottom is available as a [package](https://pkgs.alpinelinux.org/packages?name=bottom&branch=edge&repo=&arch=&origin=&flagged=&maintainer=) for Alpine Linux via `apk`:\n\n```bash\napk add bottom\n```\n\nPackages for documentation ([`bottom-doc`](https://pkgs.alpinelinux.org/packages?name=bottom-doc&branch=edge&repo=&arch=&origin=&flagged=&maintainer=)) and completions for Bash ([`bottom-bash-completion`](https://pkgs.alpinelinux.org/packages?name=bottom-bash-completion&branch=edge&repo=&arch=&origin=&flagged=&maintainer=)), Fish ([`bottom-fish-completion`](https://pkgs.alpinelinux.org/packages?name=bottom-fish-completion&branch=edge&repo=&arch=&origin=&flagged=&maintainer=)), and Zsh ([`bottom-zsh-completion`](https://pkgs.alpinelinux.org/packages?name=bottom-zsh-completion&branch=edge&repo=&arch=&origin=&flagged=&maintainer=)) are also available.\n\n### Arch Linux\n\nbottom is available as an [official package](https://archlinux.org/packages/extra/x86_64/bottom/) that can be installed with `pacman`:\n\n```bash\nsudo pacman -S bottom\n```\n\nIf you want the latest changes that are not yet stable, you can also install `bottom-git` [from the AUR](https://aur.archlinux.org/packages/bottom-git):\n\n```bash\n# Using paru\nparu -S bottom-git\n\n# Using yay\nyay -S bottom-git\n```\n\n### Debian / Ubuntu\n\nA `.deb` file is provided on each [stable release](https://github.com/ClementTsang/bottom/releases/latest) and\n[nightly builds](https://bottom.pages.dev/nightly/nightly-release) for x86, aarch64, and armv7.\nSome examples of installing it this way:\n\n```bash\n# x86-64\ncurl -LO https://github.com/ClementTsang/bottom/releases/download/0.12.3/bottom_0.12.3-1_amd64.deb\nsudo dpkg -i bottom_0.12.3-1_amd64.deb\n\n# ARM64\ncurl -LO https://github.com/ClementTsang/bottom/releases/download/0.12.3/bottom_0.12.3-1_arm64.deb\nsudo dpkg -i bottom_0.12.3-1_arm64.deb\n\n# ARM\ncurl -LO https://github.com/ClementTsang/bottom/releases/download/0.12.3/bottom_0.12.3-1_armhf.deb\nsudo dpkg -i bottom_0.12.3-1_armhf.deb\n\n# musl-based\ncurl -LO https://github.com/ClementTsang/bottom/releases/download/0.12.3/bottom-musl_0.12.3-1_amd64.deb\nsudo dpkg -i bottom-musl_0.12.3-1_amd64.deb\n```\n\n### Exherbo Linux\n\nbottom is available as a [rust package](https://gitlab.exherbo.org/exherbo/rust/-/tree/master/packages/sys-process/bottom) that can be installed with `cave`:\n\n```bash\ncave resolve -x repository/rust\ncave resolve -x bottom\n```\n\n### Fedora / CentOS / AlmaLinux / Rocky Linux\n\n#### COPR\n\n> [!WARNING]\n>\n> `atim/bottom` seems to be unmaintained and may be outdated ([relevant issue](https://github.com/ClementTsang/bottom/issues/1904))\n\nbottom is available on [COPR](https://copr.fedorainfracloud.org/coprs/atim/bottom/):\n\n```bash\nsudo dnf copr enable atim/bottom -y\nsudo dnf install bottom\n```\n\n#### Terra\n\nbottom is also available via [Terra](https://terra.fyralabs.com/):\n\n```bash\nsudo dnf install --repofrompath 'terra,https://repos.fyralabs.com/terra$releasever' --setopt='terra.gpgkey=https://repos.fyralabs.com/terra$releasever/key.asc' terra-release\nsudo dnf install bottom\n```\n\n#### RPM\n\n`.rpm` files are also generated for x86 in the [releases](https://github.com/ClementTsang/bottom/releases) page.\nFor example:\n\n```bash\ncurl -LO https://github.com/ClementTsang/bottom/releases/download/0.12.3/bottom-0.12.3-1.x86_64.rpm\nsudo dnf install ./bottom-0.12.3-1.x86_64.rpm\n```\n\n### Gentoo\n\nAvailable in the [official Gentoo repo](https://packages.gentoo.org/packages/sys-process/bottom):\n\n```bash\nsudo emerge --ask sys-process/bottom\n```\n\n### Nix\n\nAvailable [in Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=bottom&from=0&size=1&sort=relevance&type=packages) as `bottom`:\n\n```bash\nnix profile install nixpkgs#bottom\n```\n\n`bottom` can also be installed and configured through the [home-manager](https://nix-community.github.io/home-manager) module:\n\n```nix\n{\n  programs.bottom.enable = true;\n}\n```\n\n### openSUSE\n\nAvailable in openSUSE Tumbleweed:\n\n```bash\nzypper in bottom\n```\n\n### Snap\n\nbottom is available as a [snap](https://snapcraft.io/install/bottom/ubuntu):\n\n```bash\nsudo snap install bottom\n\n# To allow the program to run as intended\nsudo snap connect bottom:mount-observe\nsudo snap connect bottom:hardware-observe\nsudo snap connect bottom:system-observe\nsudo snap connect bottom:process-control\n```\n\n### Solus\n\nAvailable [in the Solus repos](https://dev.getsol.us/source/bottom/):\n\n```bash\nsudo eopkg it bottom\n```\n\n### Void\n\nAvailable [in the void-packages repo](https://github.com/void-linux/void-packages/tree/master/srcpkgs/bottom):\n\n```bash\nsudo xbps-install bottom\n```\n\n### gah\n\nbottom can also be installed on Linux or macOS using [gah](https://github.com/marverix/gah):\n\n```bash\ngah install bottom\n```\n\n### Homebrew\n\nThe formula is available [here](https://formulae.brew.sh/formula/bottom):\n\n```bash\nbrew install bottom\n```\n\n### MacPorts\n\nAvailable [here](https://ports.macports.org/port/bottom/):\n\n```bash\nsudo port selfupdate\nsudo port install bottom\n```\n\n### Chocolatey\n\nChocolatey packages are located [here](https://chocolatey.org/packages/bottom):\n\n```bash\nchoco install bottom\n```\n\n### Scoop\n\nAvailable in the [Main bucket](https://github.com/ScoopInstaller/Main):\n\n```bash\nscoop install bottom\n```\n\n### winget\n\nThe winget package can be found [here](https://github.com/microsoft/winget-pkgs/tree/master/manifests/c/Clement/bottom):\n\n```bash\nwinget install bottom\n\n# If you need a more specific app id:\nwinget install Clement.bottom\n```\n\nYou can uninstall via Control Panel, Options, or `winget --uninstall bottom`.\n\n### Windows installer\n\nYou can manually install bottom as a Windows program by downloading and using the `.msi` file from the [latest release](https://github.com/ClementTsang/bottom/releases/latest).\n\n### Conda\n\nYou can install bottom using `conda` with [this conda-smithy repository](https://github.com/conda-forge/bottom-feedstock):\n\n```bash\n# Add the channel\nconda config --add channels conda-forge\nconda config --set channel_priority strict\n\n# Install\nconda install bottom\n```\n\n### mise\n\nbottom is available in [mise](https://github.com/jdx/mise). You can install it like so:\n\n```\nmise use -g bottom@latest\n```\n\n### Pre-built binaries\n\nYou can also use the pre-built release binaries:\n\n- [Latest stable release](https://github.com/ClementTsang/bottom/releases/latest)\n- [Latest nightly release](https://bottom.pages.dev/nightly/nightly-release)\n\nTo use, download and extract the binary that matches your system. You can then run by doing:\n\n```bash\n./btm\n```\n\nor by installing to your system following the procedures for installing binaries to your system.\n\n#### Auto-completion\n\nThe release binaries in [the releases page](https://github.com/ClementTsang/bottom/releases) are packaged with\nshell auto-completion files for Bash, Zsh, fish, Powershell, Elvish, Fig, and Nushell. To install them:\n\n- For Bash, move `btm.bash` to `$XDG_CONFIG_HOME/bash_completion or /etc/bash_completion.d/`.\n- For Zsh, move `_btm` to one of your `$fpath` directories.\n- For fish, move `btm.fish` to `$HOME/.config/fish/completions/`.\n- For PowerShell, add `_btm.ps1` to your PowerShell [profile](<https://docs.microsoft.com/en-us/previous-versions//bb613488(v=vs.85)>).\n- For Elvish, the completion file is `btm.elv`.\n- For Fig, the completion file is `btm.ts`.\n- For Nushell, source `btm.nu`.\n\nThe individual auto-completion files are also included in the stable/nightly releases as `completion.tar.gz` if needed.\n\n## Usage\n\nYou can run bottom using `btm`.\n\n- For help on flags, use `btm -h` for a quick overview or `btm --help` for more details.\n- For info on key and mouse bindings, press `?` inside bottom or refer to the [documentation page](https://bottom.pages.dev/nightly/).\n\nYou can find more information on usage in the [documentation](https://bottom.pages.dev/nightly/).\n\n## Configuration\n\nbottom accepts a number of command-line arguments to change the behaviour of the application as desired.\nAdditionally, bottom will automatically generate a configuration file on the first launch, which can be changed.\n\nMore details on configuration can be found [in the documentation](https://bottom.pages.dev/nightly/configuration/config-file/).\n\n## Troubleshooting\n\nIf some things aren't working, give the [troubleshooting page](https://bottom.pages.dev/nightly/troubleshooting)\na look. If things still aren't working, then consider asking [a question](https://github.com/ClementTsang/bottom/discussions)\nor filing a [bug report](https://github.com/ClementTsang/bottom/issues/new/choose) if you think it's a bug.\n\n## Documentation\n\nThe main documentation page can be found at <https://bottom.pages.dev>, using Cloudflare Pages. If needed, a mirror hosted using\nGithub Pages is also available at <https://clementtsang.github.io/bottom>.\n\n## Contribution\n\nWhether it's reporting bugs, suggesting features, maintaining packages, or submitting a PR, contribution is always welcome! Please read\n[CONTRIBUTING.md](./CONTRIBUTING.md) for details on how to contribute to bottom.\n\n### Contributors\n\nThanks to all contributors:\n\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- prettier-ignore-start -->\n<!-- markdownlint-disable -->\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://shilangyu.github.io\"><img src=\"https://avatars3.githubusercontent.com/u/29288116?v=4?s=100\" width=\"100px;\" alt=\"Marcin Wojnarowski\"/><br /><sub><b>Marcin Wojnarowski</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=shilangyu\" title=\"Code\">💻</a> <a href=\"#platform-shilangyu\" title=\"Packaging/porting to new platform\">📦</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://neosmart.net/\"><img src=\"https://avatars3.githubusercontent.com/u/606923?v=4?s=100\" width=\"100px;\" alt=\"Mahmoud Al-Qudsi\"/><br /><sub><b>Mahmoud Al-Qudsi</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=mqudsi\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://andys8.de\"><img src=\"https://avatars0.githubusercontent.com/u/13085980?v=4?s=100\" width=\"100px;\" alt=\"Andy\"/><br /><sub><b>Andy</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=andys8\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/HarHarLinks\"><img src=\"https://avatars0.githubusercontent.com/u/2803622?v=4?s=100\" width=\"100px;\" alt=\"Kim Brose\"/><br /><sub><b>Kim Brose</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=HarHarLinks\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://svenstaro.org\"><img src=\"https://avatars0.githubusercontent.com/u/1664?v=4?s=100\" width=\"100px;\" alt=\"Sven-Hendrik Haase\"/><br /><sub><b>Sven-Hendrik Haase</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=svenstaro\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://liberapay.com/Artem4/\"><img src=\"https://avatars0.githubusercontent.com/u/5614476?v=4?s=100\" width=\"100px;\" alt=\"Artem Polishchuk\"/><br /><sub><b>Artem Polishchuk</b></sub></a><br /><a href=\"#platform-tim77\" title=\"Packaging/porting to new platform\">📦</a> <a href=\"https://github.com/ClementTsang/bottom/commits?author=tim77\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://ruby-journal.com/\"><img src=\"https://avatars2.githubusercontent.com/u/135605?v=4?s=100\" width=\"100px;\" alt=\"Trung Lê\"/><br /><sub><b>Trung Lê</b></sub></a><br /><a href=\"#platform-runlevel5\" title=\"Packaging/porting to new platform\">📦</a> <a href=\"#infra-runlevel5\" title=\"Infrastructure (Hosting, Build-Tools, etc)\">🚇</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/dm9pZCAq\"><img src=\"https://avatars1.githubusercontent.com/u/46228973?v=4?s=100\" width=\"100px;\" alt=\"dm9pZCAq\"/><br /><sub><b>dm9pZCAq</b></sub></a><br /><a href=\"#platform-dm9pZCAq\" title=\"Packaging/porting to new platform\">📦</a> <a href=\"https://github.com/ClementTsang/bottom/commits?author=dm9pZCAq\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://lukor.org\"><img src=\"https://avatars2.githubusercontent.com/u/10536802?v=4?s=100\" width=\"100px;\" alt=\"Lukas Rysavy\"/><br /><sub><b>Lukas Rysavy</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=LlinksRechts\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://hamberg.no/erlend\"><img src=\"https://avatars3.githubusercontent.com/u/16063?v=4?s=100\" width=\"100px;\" alt=\"Erlend Hamberg\"/><br /><sub><b>Erlend Hamberg</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=ehamberg\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://onee3.org\"><img src=\"https://avatars.githubusercontent.com/u/4507647?v=4?s=100\" width=\"100px;\" alt=\"Frederick Zhang\"/><br /><sub><b>Frederick Zhang</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=Frederick888\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/pvanheus\"><img src=\"https://avatars.githubusercontent.com/u/4154788?v=4?s=100\" width=\"100px;\" alt=\"pvanheus\"/><br /><sub><b>pvanheus</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=pvanheus\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/briandipalma\"><img src=\"https://avatars.githubusercontent.com/u/1597820?v=4?s=100\" width=\"100px;\" alt=\"Brian Di Palma\"/><br /><sub><b>Brian Di Palma</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=briandipalma\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://dakyskye.github.io\"><img src=\"https://avatars.githubusercontent.com/u/32128756?v=4?s=100\" width=\"100px;\" alt=\"Lasha Kanteladze\"/><br /><sub><b>Lasha Kanteladze</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=dakyskye\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/herbygillot\"><img src=\"https://avatars.githubusercontent.com/u/618376?v=4?s=100\" width=\"100px;\" alt=\"Herby Gillot\"/><br /><sub><b>Herby Gillot</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=herbygillot\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/yellowsquid\"><img src=\"https://avatars.githubusercontent.com/u/46519298?v=4?s=100\" width=\"100px;\" alt=\"Greg Brown\"/><br /><sub><b>Greg Brown</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=yellowsquid\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/TotalCaesar659\"><img src=\"https://avatars.githubusercontent.com/u/14265316?v=4?s=100\" width=\"100px;\" alt=\"TotalCaesar659\"/><br /><sub><b>TotalCaesar659</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=TotalCaesar659\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/grawlinson\"><img src=\"https://avatars.githubusercontent.com/u/4408051?v=4?s=100\" width=\"100px;\" alt=\"George Rawlinson\"/><br /><sub><b>George Rawlinson</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=grawlinson\" title=\"Documentation\">📖</a> <a href=\"#platform-grawlinson\" title=\"Packaging/porting to new platform\">📦</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.frogorbits.com/\"><img src=\"https://avatars.githubusercontent.com/u/101246?v=4?s=100\" width=\"100px;\" alt=\"adiabatic\"/><br /><sub><b>adiabatic</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=adiabatic\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://electronsweatshop.com\"><img src=\"https://avatars.githubusercontent.com/u/354506?v=4?s=100\" width=\"100px;\" alt=\"Randy Barlow\"/><br /><sub><b>Randy Barlow</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=bowlofeggs\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://jackson.dev\"><img src=\"https://avatars.githubusercontent.com/u/160646?v=4?s=100\" width=\"100px;\" alt=\"Patrick Jackson\"/><br /><sub><b>Patrick Jackson</b></sub></a><br /><a href=\"#ideas-patricksjackson\" title=\"Ideas, Planning, & Feedback\">🤔</a> <a href=\"https://github.com/ClementTsang/bottom/commits?author=patricksjackson\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/mati865\"><img src=\"https://avatars.githubusercontent.com/u/1174646?v=4?s=100\" width=\"100px;\" alt=\"Mateusz Mikuła\"/><br /><sub><b>Mateusz Mikuła</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=mati865\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://blog.guillaume-gomez.fr\"><img src=\"https://avatars.githubusercontent.com/u/3050060?v=4?s=100\" width=\"100px;\" alt=\"Guillaume Gomez\"/><br /><sub><b>Guillaume Gomez</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=GuillaumeGomez\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/shurizzle\"><img src=\"https://avatars.githubusercontent.com/u/203655?v=4?s=100\" width=\"100px;\" alt=\"shura\"/><br /><sub><b>shura</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=shurizzle\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.wezm.net/\"><img src=\"https://avatars.githubusercontent.com/u/21787?v=4?s=100\" width=\"100px;\" alt=\"Wesley Moore\"/><br /><sub><b>Wesley Moore</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=wezm\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/xgdgsc\"><img src=\"https://avatars.githubusercontent.com/u/1189869?v=4?s=100\" width=\"100px;\" alt=\"xgdgsc\"/><br /><sub><b>xgdgsc</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=xgdgsc\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/ViridiCanis\"><img src=\"https://avatars.githubusercontent.com/u/49595344?v=4?s=100\" width=\"100px;\" alt=\"ViridiCanis\"/><br /><sub><b>ViridiCanis</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=ViridiCanis\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/jamartin9\"><img src=\"https://avatars.githubusercontent.com/u/7027701?v=4?s=100\" width=\"100px;\" alt=\"Justin Martin\"/><br /><sub><b>Justin Martin</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=jamartin9\" title=\"Code\">💻</a> <a href=\"https://github.com/ClementTsang/bottom/commits?author=jamartin9\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/DianaNites\"><img src=\"https://avatars.githubusercontent.com/u/5275194?v=4?s=100\" width=\"100px;\" alt=\"Diana\"/><br /><sub><b>Diana</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=DianaNites\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://hervyqa.id\"><img src=\"https://avatars.githubusercontent.com/u/45872139?v=4?s=100\" width=\"100px;\" alt=\"Hervy Qurrotul Ainur Rozi\"/><br /><sub><b>Hervy Qurrotul Ainur Rozi</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=hervyqa\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://mrivnak.github.io\"><img src=\"https://avatars.githubusercontent.com/u/7389355?v=4?s=100\" width=\"100px;\" alt=\"Mike Rivnak\"/><br /><sub><b>Mike Rivnak</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=mrivnak\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/lroobrou\"><img src=\"https://avatars.githubusercontent.com/u/35152113?v=4?s=100\" width=\"100px;\" alt=\"lroobrou\"/><br /><sub><b>lroobrou</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=lroobrou\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://cube64128.xyz/\"><img src=\"https://avatars.githubusercontent.com/u/18757988?v=4?s=100\" width=\"100px;\" alt=\"database64128\"/><br /><sub><b>database64128</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=database64128\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/sou-chon\"><img src=\"https://avatars.githubusercontent.com/u/35537528?v=4?s=100\" width=\"100px;\" alt=\"Chon Sou\"/><br /><sub><b>Chon Sou</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=sou-chon\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/Drsheppard01\"><img src=\"https://avatars.githubusercontent.com/u/60893791?v=4?s=100\" width=\"100px;\" alt=\"DrSheppard\"/><br /><sub><b>DrSheppard</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=Drsheppard01\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/RaresCon\"><img src=\"https://avatars.githubusercontent.com/u/95525840?v=4?s=100\" width=\"100px;\" alt=\"Rareș Constantin\"/><br /><sub><b>Rareș Constantin</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=RaresCon\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://felipesuri.com\"><img src=\"https://avatars.githubusercontent.com/u/50281523?v=4?s=100\" width=\"100px;\" alt=\"felipesuri\"/><br /><sub><b>felipesuri</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=felipesuri\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/spital\"><img src=\"https://avatars.githubusercontent.com/u/11034264?v=4?s=100\" width=\"100px;\" alt=\"spital\"/><br /><sub><b>spital</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=spital\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://bikodbg.com/\"><img src=\"https://avatars.githubusercontent.com/u/1389811?v=4?s=100\" width=\"100px;\" alt=\"Michael Bikovitsky\"/><br /><sub><b>Michael Bikovitsky</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=mbikovitsky\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/dvalter\"><img src=\"https://avatars.githubusercontent.com/u/38795282?v=4?s=100\" width=\"100px;\" alt=\"Dmitry Valter\"/><br /><sub><b>Dmitry Valter</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=dvalter\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/aragonnetje6\"><img src=\"https://avatars.githubusercontent.com/u/69118097?v=4?s=100\" width=\"100px;\" alt=\"Grace Stok\"/><br /><sub><b>Grace Stok</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=aragonnetje6\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/yshui\"><img src=\"https://avatars.githubusercontent.com/u/366851?v=4?s=100\" width=\"100px;\" alt=\"Yuxuan Shui\"/><br /><sub><b>Yuxuan Shui</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=yshui\" title=\"Code\">💻</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://zongwenqing.com\"><img src=\"https://avatars.githubusercontent.com/u/43934749?v=4?s=100\" width=\"100px;\" alt=\"Wenqing Zong\"/><br /><sub><b>Wenqing Zong</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=WenqingZong\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://gabelluardo.github.io\"><img src=\"https://avatars.githubusercontent.com/u/42920247?v=4?s=100\" width=\"100px;\" alt=\"Gabriele Belluardo\"/><br /><sub><b>Gabriele Belluardo</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=gabelluardo\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://zebulon.dev/\"><img src=\"https://avatars.githubusercontent.com/u/14242997?v=4?s=100\" width=\"100px;\" alt=\"Zeb Piasecki\"/><br /><sub><b>Zeb Piasecki</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=zebp\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://freed-wu.github.io/\"><img src=\"https://avatars.githubusercontent.com/u/32936898?v=4?s=100\" width=\"100px;\" alt=\"wzy\"/><br /><sub><b>wzy</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=Freed-Wu\" title=\"Code\">💻</a> <a href=\"https://github.com/ClementTsang/bottom/commits?author=Freed-Wu\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://johnlin.ca/\"><img src=\"https://avatars.githubusercontent.com/u/66440371?v=4?s=100\" width=\"100px;\" alt=\"john-s-lin\"/><br /><sub><b>john-s-lin</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=john-s-lin\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/lyuha\"><img src=\"https://avatars.githubusercontent.com/u/4014016?v=4?s=100\" width=\"100px;\" alt=\"Lee Wonjoon\"/><br /><sub><b>Lee Wonjoon</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=lyuha\" title=\"Code\">💻</a> <a href=\"https://github.com/ClementTsang/bottom/commits?author=lyuha\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.davlgd.fr\"><img src=\"https://avatars.githubusercontent.com/u/1110600?v=4?s=100\" width=\"100px;\" alt=\"David Legrand\"/><br /><sub><b>David Legrand</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=davlgd\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/MichalBryxi\"><img src=\"https://avatars.githubusercontent.com/u/847473?v=4?s=100\" width=\"100px;\" alt=\"Michal Bryxí\"/><br /><sub><b>Michal Bryxí</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=MichalBryxi\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://mpia.de/~hviding/\"><img src=\"https://avatars.githubusercontent.com/u/17031860?v=4?s=100\" width=\"100px;\" alt=\"Raphael Erik Hviding\"/><br /><sub><b>Raphael Erik Hviding</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=TheSkyentist\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://cosmichorror.dev\"><img src=\"https://avatars.githubusercontent.com/u/30302768?v=4?s=100\" width=\"100px;\" alt=\"CosmicHorror\"/><br /><sub><b>CosmicHorror</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=CosmicHorrorDev\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.woods.am/\"><img src=\"https://avatars.githubusercontent.com/u/7113557?v=4?s=100\" width=\"100px;\" alt=\"Ben Woods\"/><br /><sub><b>Ben Woods</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=woodsb02\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://cgdct.moe\"><img src=\"https://avatars.githubusercontent.com/u/20411956?v=4?s=100\" width=\"100px;\" alt=\"Stephen Huan\"/><br /><sub><b>Stephen Huan</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=stephen-huan\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/jasongwartz\"><img src=\"https://avatars.githubusercontent.com/u/10981911?v=4?s=100\" width=\"100px;\" alt=\"Jason Gwartz\"/><br /><sub><b>Jason Gwartz</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=jasongwartz\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/llc0930\"><img src=\"https://avatars.githubusercontent.com/u/14966910?v=4?s=100\" width=\"100px;\" alt=\"llc0930\"/><br /><sub><b>llc0930</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=llc0930\" title=\"Code\">💻</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://chronovore.dev\"><img src=\"https://avatars.githubusercontent.com/u/614231?v=4?s=100\" width=\"100px;\" alt=\"Ada Ahmed\"/><br /><sub><b>Ada Ahmed</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=yretenai\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/Wateir\"><img src=\"https://avatars.githubusercontent.com/u/78731687?v=4?s=100\" width=\"100px;\" alt=\"Wateir\"/><br /><sub><b>Wateir</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=Wateir\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/al42and\"><img src=\"https://avatars.githubusercontent.com/u/933873?v=4?s=100\" width=\"100px;\" alt=\"Andrey Alekseenko\"/><br /><sub><b>Andrey Alekseenko</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=al42and\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://fgimian.github.io/\"><img src=\"https://avatars.githubusercontent.com/u/1811813?v=4?s=100\" width=\"100px;\" alt=\"Fotis Gimian\"/><br /><sub><b>Fotis Gimian</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=fgimian\" title=\"Code\">💻</a> <a href=\"https://github.com/ClementTsang/bottom/commits?author=fgimian\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://sigmasquadron.net\"><img src=\"https://avatars.githubusercontent.com/u/174749595?v=4?s=100\" width=\"100px;\" alt=\"Fernando Rodrigues\"/><br /><sub><b>Fernando Rodrigues</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=SigmaSquadron\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://mtoohey.com\"><img src=\"https://avatars.githubusercontent.com/u/36740602?v=4?s=100\" width=\"100px;\" alt=\"Matthew Toohey\"/><br /><sub><b>Matthew Toohey</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=mtoohey31\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://meander.site\"><img src=\"https://avatars.githubusercontent.com/u/11584387?v=4?s=100\" width=\"100px;\" alt=\"Julius Enriquez\"/><br /><sub><b>Julius Enriquez</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=win8linux\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/benjamb\"><img src=\"https://avatars.githubusercontent.com/u/8291297?v=4?s=100\" width=\"100px;\" alt=\"Ben Brown\"/><br /><sub><b>Ben Brown</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=benjamb\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/nyurik\"><img src=\"https://avatars.githubusercontent.com/u/1641515?v=4?s=100\" width=\"100px;\" alt=\"Yuri Astrakhan\"/><br /><sub><b>Yuri Astrakhan</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=nyurik\" title=\"Code\">💻</a> <a href=\"https://github.com/ClementTsang/bottom/commits?author=nyurik\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://kachick.github.io/\"><img src=\"https://avatars.githubusercontent.com/u/1180335?v=4?s=100\" width=\"100px;\" alt=\"Kenichi Kamiya\"/><br /><sub><b>Kenichi Kamiya</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=kachick\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/yahlia\"><img src=\"https://avatars.githubusercontent.com/u/40295453?v=4?s=100\" width=\"100px;\" alt=\"yahlia\"/><br /><sub><b>yahlia</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=yahlia\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/Bucket-Bucket-Bucket\"><img src=\"https://avatars.githubusercontent.com/u/107044719?v=4?s=100\" width=\"100px;\" alt=\"Bucket-Bucket-Bucket\"/><br /><sub><b>Bucket-Bucket-Bucket</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=Bucket-Bucket-Bucket\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://marek.sierocinscy.pl\"><img src=\"https://avatars.githubusercontent.com/u/2142811?v=4?s=100\" width=\"100px;\" alt=\"Marek Sierociński\"/><br /><sub><b>Marek Sierociński</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=marverix\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/Tommimon\"><img src=\"https://avatars.githubusercontent.com/u/37435103?v=4?s=100\" width=\"100px;\" alt=\"Tommaso Montanari\"/><br /><sub><b>Tommaso Montanari</b></sub></a><br /><a href=\"#design-Tommimon\" title=\"Design\">🎨</a> <a href=\"#ideas-Tommimon\" title=\"Ideas, Planning, & Feedback\">🤔</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://blog.lenhof.eu.org\"><img src=\"https://avatars.githubusercontent.com/u/36410287?v=4?s=100\" width=\"100px;\" alt=\"Jean-Yves LENHOF\"/><br /><sub><b>Jean-Yves LENHOF</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=jylenhof\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"http://saphereye.github.io\"><img src=\"https://avatars.githubusercontent.com/u/59739923?v=4?s=100\" width=\"100px;\" alt=\"Adarsh Das\"/><br /><sub><b>Adarsh Das</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=Saphereye\" title=\"Code\">💻</a> <a href=\"https://github.com/ClementTsang/bottom/commits?author=Saphereye\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/oxyzenQ\"><img src=\"https://avatars.githubusercontent.com/u/130107241?v=4?s=100\" width=\"100px;\" alt=\"rezky_nightky\"/><br /><sub><b>rezky_nightky</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=oxyzenQ\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/gitgoggles\"><img src=\"https://avatars.githubusercontent.com/u/101480183?v=4?s=100\" width=\"100px;\" alt=\"gitgoggles\"/><br /><sub><b>gitgoggles</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=gitgoggles\" title=\"Code\">💻</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/thunze\"><img src=\"https://avatars.githubusercontent.com/u/22795263?v=4?s=100\" width=\"100px;\" alt=\"Tom\"/><br /><sub><b>Tom</b></sub></a><br /><a href=\"#maintenance-thunze\" title=\"Maintenance\">🚧</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/ggaddy\"><img src=\"https://avatars.githubusercontent.com/u/13815367?v=4?s=100\" width=\"100px;\" alt=\"G\"/><br /><sub><b>G</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=ggaddy\" title=\"Documentation\">📖</a></td>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/lpnh\"><img src=\"https://avatars.githubusercontent.com/u/90577992?v=4?s=100\" width=\"100px;\" alt=\"Filipe Paniguel\"/><br /><sub><b>Filipe Paniguel</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=lpnh\" title=\"Documentation\">📖</a></td>\n    </tr>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://www.jianshu.com/u/f5754cd2e83d\"><img src=\"https://avatars.githubusercontent.com/u/15232241?v=4?s=100\" width=\"100px;\" alt=\"Qiying Wang\"/><br /><sub><b>Qiying Wang</b></sub></a><br /><a href=\"https://github.com/ClementTsang/bottom/commits?author=WqyJh\" title=\"Code\">💻</a></td>\n    </tr>\n  </tbody>\n</table>\n\n<!-- markdownlint-restore -->\n<!-- prettier-ignore-end -->\n\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n\n## Thanks\n\n- This project is very much inspired by [gotop](https://github.com/xxxserxxx/gotop),\n  [gtop](https://github.com/aksakalli/gtop), and [htop](https://github.com/htop-dev/htop/).\n\n- This application was written with [many](https://github.com/ClementTsang/bottom/blob/main/Cargo.toml),\n  [_many_ libraries](https://github.com/ClementTsang/bottom/blob/main/Cargo.lock), as well as many services and\n  programs, all built on top of the work of many talented people. bottom would not exist without all of this.\n\n- And of course, thank you again to all contributors and package maintainers!\n\n- I also really appreciate anyone who has used bottom, and those\n  who go out of their way to report bugs or suggest ways to improve things. I hope\n  it's been a useful tool for others.\n\n- To those who support my work financially via donations, thank you so much.\n\n- Thanks to JetBrains for providing access to tools that I use to develop bottom\n  as part of their [open source support program](https://jb.gg/OpenSourceSupport).\n\n  <a href=\"https://jb.gg/OpenSourceSupport\">\n    <img src=\"https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg\" alt=\"JetBrains logo\" width=\"150\" />\n  </a>\n\n- Also thanks to [SignPath.io](https://about.signpath.io/) for providing a free code signing service, and to the\n  [SignPath Foundation](https://signpath.org/) for the certificate.\n"
  },
  {
    "path": "build.rs",
    "content": "//! General build script used by bottom to generate completion files and set binary version.\n\n#![expect(clippy::unwrap_used)]\n\n#[expect(dead_code)]\n#[path = \"src/options/args.rs\"]\nmod args;\n\nuse std::{\n    env, fs, io,\n    path::{Path, PathBuf},\n};\n\nuse clap::{Command, CommandFactory};\nuse clap_complete::{Generator, generate_to, shells::Shell};\nuse clap_complete_fig::Fig;\nuse clap_complete_nushell::Nushell;\n\nuse crate::args::BottomArgs;\n\nfn create_dir(dir: &Path) -> io::Result<()> {\n    fs::create_dir_all(dir).inspect_err(|err| {\n        eprintln!(\n            \"Couldn't create a directory at {} ({:?}). Aborting.\",\n            dir.display(),\n            err\n        )\n    })\n}\n\nfn generate_completions<G>(to_generate: G, cmd: &mut Command, out_dir: &Path) -> io::Result<PathBuf>\nwhere\n    G: Generator,\n{\n    generate_to(to_generate, cmd, \"btm\", out_dir)\n}\n\nfn btm_generate() -> io::Result<()> {\n    const ENV_KEY: &str = \"BTM_GENERATE\";\n\n    match env::var_os(ENV_KEY) {\n        Some(var) if !var.is_empty() => {\n            let completion_dir =\n                option_env!(\"COMPLETION_DIR\").unwrap_or(\"./target/tmp/bottom/completion/\");\n            let manpage_dir = option_env!(\"MANPAGE_DIR\").unwrap_or(\"./target/tmp/bottom/manpage/\");\n\n            let completion_out_dir = PathBuf::from(completion_dir);\n            let manpage_out_dir = PathBuf::from(manpage_dir);\n\n            create_dir(&completion_out_dir)?;\n            create_dir(&manpage_out_dir)?;\n\n            // Generate completions\n            let mut app = BottomArgs::command();\n            generate_completions(Shell::Bash, &mut app, &completion_out_dir)?;\n            generate_completions(Shell::Zsh, &mut app, &completion_out_dir)?;\n            generate_completions(Shell::Fish, &mut app, &completion_out_dir)?;\n            generate_completions(Shell::PowerShell, &mut app, &completion_out_dir)?;\n            generate_completions(Shell::Elvish, &mut app, &completion_out_dir)?;\n            generate_completions(Fig, &mut app, &completion_out_dir)?;\n            generate_completions(Nushell, &mut app, &completion_out_dir)?;\n\n            // Generate manpage\n            let app = app.name(\"btm\");\n            let man = clap_mangen::Man::new(app);\n            let mut buffer: Vec<u8> = Default::default();\n            man.render(&mut buffer)?;\n            fs::write(manpage_out_dir.join(\"btm.1\"), buffer)?;\n        }\n        _ => {}\n    }\n\n    println!(\"cargo:rerun-if-env-changed={ENV_KEY}\");\n\n    Ok(())\n}\n\nfn extract_sha(sha: Option<&str>) -> Option<&str> {\n    sha.and_then(|sha: &str| sha.get(0..8))\n}\n\nfn output_nightly_version(version: &str, git_hash: &str) {\n    println!(\"cargo:rustc-env=NIGHTLY_VERSION={version}-nightly-{git_hash}\");\n}\n\nfn nightly_version() {\n    const ENV_KEY: &str = \"BTM_BUILD_RELEASE_CALLER\";\n\n    match env::var_os(ENV_KEY) {\n        Some(var) if !var.is_empty() && var == \"ci\" => {\n            let version = env!(\"CARGO_PKG_VERSION\");\n\n            if let Some(hash) = extract_sha(option_env!(\"CIRRUS_CHANGE_IN_REPO\")) {\n                // May be set if we're building with Cirrus CI.\n                output_nightly_version(version, hash);\n            } else if let Some(hash) = extract_sha(option_env!(\"GITHUB_SHA\")) {\n                // May be set if we're building with GHA.\n                output_nightly_version(version, hash);\n            } else if let Ok(output) = std::process::Command::new(\"git\")\n                .args([\"rev-parse\", \"--short=8\", \"HEAD\"])\n                .output()\n            {\n                // If we're not building in either, we do the lazy thing and fall back to\n                // manually grabbing info using git as a command.\n                let hash = String::from_utf8(output.stdout).unwrap();\n                output_nightly_version(version, &hash);\n            }\n        }\n        _ => {}\n    }\n\n    println!(\"cargo:rerun-if-env-changed={ENV_KEY}\");\n    println!(\"cargo:rerun-if-env-changed=CIRRUS_CHANGE_IN_REPO\");\n}\n\nfn main() -> io::Result<()> {\n    btm_generate()?;\n    nightly_version();\n\n    Ok(())\n}\n"
  },
  {
    "path": "clippy.toml",
    "content": "cognitive-complexity-threshold = 100\ntype-complexity-threshold = 500\ntoo-many-arguments-threshold = 8\nallow-unwrap-in-consts = true\nallow-unwrap-in-tests = true\n"
  },
  {
    "path": "codecov.yml",
    "content": "coverage:\n  status:\n    project:\n      default:\n        target: auto\n        threshold: 30%\n    patch: off\n"
  },
  {
    "path": "desktop/bottom.desktop",
    "content": "[Desktop Entry]\nName=bottom\nVersion=1.5\nGenericName=System Monitor\nComment=A customizable cross-platform graphical process/system monitor for the terminal.\nExec=btm\nTerminal=true\nType=Application\nCategories=System;ConsoleOnly;Monitor;\nStartupNotify=false\nIcon=bottom-system-monitor"
  },
  {
    "path": "docs/.gitignore",
    "content": "site/\nvenv/\n.cache/\nhooks/__pycache__/"
  },
  {
    "path": "docs/README.md",
    "content": "# Extended Documentation\n\nThis is where the extended documentation resides, hosted on GitHub Pages. We use [MkDocs](https://www.mkdocs.org/),\n[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/), and [mike](https://github.com/jimporter/mike).\n\nDocumentation is currently built using Python 3.11, though it should work fine with older versions.\n\n## Running locally\n\nOne way is to just run `serve.sh`. Alternatively, the manual steps are, assuming your current working directory\nis the bottom repo:\n\n```bash\n# Change directories to the documentation.\ncd docs/\n\n# Create and activate venv.\npython -m venv venv\nsource venv/bin/activate\n\n# Install requirements\npip install -r requirements.txt\n\n# Run mkdocs\nvenv/bin/mkdocs serve\n```\n\n## Deploying\n\nDeploying is done via [mike](https://github.com/jimporter/mike) in order to get versioning. Typically,\nthis is done through CI, but can be done manually if needed.\n\n### Nightly docs\n\n```bash\ncd docs\nmike deploy nightly --push\n```\n\n### Stable docs\n\n```bash\ncd docs\n\n# Rename the previous stable version\nmike retitle --push stable $OLD_STABLE_VERSION\n\n# Set the newest version as the most recent stable version\nmike deploy --push --update-aliases $RELEASE_VERSION stable\n\n# Append a \"(stable)\" string to the end.\nmike retitle --push $RELEASE_VERSION \"$RELEASE_VERSION (stable)\"\n```\n"
  },
  {
    "path": "docs/content/configuration/command-line-options.md",
    "content": "# Command-line Options\n\nThe following options can be provided to bottom in the command line to change the behaviour of the program. You can also\nsee information on these options by running `btm -h`, or run `btm --help` to display more detailed information on each option:\n\n## General Options\n\n| Option                              | Behaviour                                                  |\n| ----------------------------------- | ---------------------------------------------------------- |\n| `--autohide_time`                   | Temporarily shows the time scale in graphs.                |\n| `-b`, `--basic`                     | Hides graphs and uses a more basic look.                   |\n| `-C`, `--config_location <PATH>`    | Sets the location of the config file.                      |\n| `-t`, `--default_time_value <TIME>` | Default time value for graphs.                             |\n| `--default_widget_count <N>`        | Sets the N'th selected widget type as the default.         |\n| `--default_widget_type <WIDGET>`    | Sets the default widget type. Use --help for more info.    |\n| `--disable_click`                   | Disables mouse clicks.                                     |\n| `--disable_keys`                    | Disables keyboard shortcuts, INCLUDING the ones that stop  |\n|                                     | bottom.                                                    |\n| `-m`, `--dot_marker`                | Uses a dot marker for graphs.                              |\n| `-e`, `--expanded`                  | Expand the default widget upon starting the app.           |\n| `--hide_table_gap`                  | Hides spacing between table headers and entries.           |\n| `--hide_time`                       | Hides the time scale from being shown.                     |\n| `-r`, `--rate <TIME>`               | Sets how often data is refreshed.                          |\n| `--retention <TIME>`                | How far back data will be stored up to.                    |\n| `--show_table_scroll_position`      | Shows the list scroll position tracker in the widget title |\n|                                     | for table widgets.                                         |\n| `-d`, `--time_delta <TIME>`         | The amount of time changed upon zooming.                   |\n\n## Process Options\n\n| Option                      | Behaviour                                                                              |\n| --------------------------- | -------------------------------------------------------------------------------------- |\n| `-S, --case_sensitive`      | Enables case sensitivity by default when searching.                                    |\n| `-u, --current_usage`       | Calculates process CPU usage as a percentage of current usage rather than total usage. |\n| `--disable_advanced_kill`   | Hides additional stopping options on Unix-like systems.                                |\n| `--read_only`               | Prevents performing any actions that affect the system (e.g. stopping processes).      |\n| `--get_threads`             | Also gather process thread information.                                                |\n| `-g, --group_processes`     | Groups processes with the same name by default. No effect if `--tree` is set.          |\n| `--hide_k_threads`          | Hide kernel threads by default.                                                        |\n| `--process_memory_as_value` | Defaults to showing process memory usage by value.                                     |\n| `--process_command`         | Shows the full command name instead of the process name by default.                    |\n| `-R, --regex`               | Enables regex by default while searching.                                              |\n| `-T, --tree`                | Makes the process widget use tree mode by default.                                     |\n| `--tree_collapse`           | Collapse process tree by default.                                                      |\n| `-n, --unnormalized_cpu`    | Show process CPU% usage without averaging over the number of CPU cores.                |\n| `-W, --whole_word`          | Enables whole-word matching by default while searching.                                |\n\n## Temperature Options\n\n| Option             | Behaviour                                     |\n| ------------------ | --------------------------------------------- |\n| `-c, --celsius`    | Use Celsius as the temperature unit. Default. |\n| `-f, --fahrenheit` | Use Fahrenheit as the temperature unit.       |\n| `-k, --kelvin`     | Use Kelvin as the temperature unit.           |\n\n## CPU Options\n\n| Option                | Behaviour                                         |\n| --------------------- | ------------------------------------------------- |\n| `--cpu_left_legend`   | Puts the CPU chart legend on the left side.       |\n| `--default_cpu_entry` | Sets which CPU entry type is selected by default. |\n| `-a, --hide_avg_cpu`  | Hides the average CPU usage entry.                |\n\n## Memory Options\n\n| Option                       | Behaviour                                                 |\n| ---------------------------- | --------------------------------------------------------- |\n| `--memory_legend <POSITION>` | Where to place the legend for the memory chart widget.    |\n| `--enable_cache_memory`      | Enable collecting and displaying cache and buffer memory. |\n| `--free_arc`                 | Subtract freeable ARC from memory.                        |\n\n## Network Options\n\n| Option                        | Behaviour                                               |\n| ----------------------------- | ------------------------------------------------------- |\n| `--network_legend <POSITION>` | Where to place the legend for the network chart widget. |\n| `--network_use_bytes`         | Displays the network widget using bytes.                |\n| `--network_use_binary_prefix` | Displays the network widget with binary prefixes.       |\n| `--network_use_log`           | Displays the network widget with a log scale.           |\n| `--use_old_network_legend`    | (DEPRECATED) Uses a separate network legend.            |\n\n## Battery Options\n\n| Option      | Behaviour                                       |\n| ----------- | ----------------------------------------------- |\n| `--battery` | Shows the battery widget in non-custom layouts. |\n\n## GPU Options\n\n| Option          | Behaviour                                                         |\n| --------------- | ----------------------------------------------------------------- |\n| `--disable_gpu` | Disable collecting and displaying NVIDIA and AMD GPU information. |\n\n## Style Options\n\n| Option             | Behaviour                                                        |\n| ------------------ | ---------------------------------------------------------------- |\n| `--theme <SCHEME>` | Use a built-in color theme, use '--help' for info on the colors. |\n\n## Other Options\n\n| Option            | Behaviour                                         |\n| ----------------- | ------------------------------------------------- |\n| `-h`, `--help`    | Prints help info (for more details use `--help`.) |\n| `-V`, `--version` | Prints version information.                       |\n"
  },
  {
    "path": "docs/content/configuration/config-file/cpu.md",
    "content": "# CPU\n\n## Default CPU Graph Selection\n\nYou can configure which CPU graph is shown by default when starting up bottom by setting `cpu.default`.\n\n```toml\n[cpu]\n# One of \"all\" (default), \"average\"/\"avg\"\ndefault = \"average\"\n```\n"
  },
  {
    "path": "docs/content/configuration/config-file/disk-table.md",
    "content": "# Disk Table\n\n## Columns\n\nYou can configure which columns are shown by the disk table widget by setting the `columns` setting:\n\n```toml\n[disk]\n# Pick which columns you want to use in any order.\ncolumns = [\"Disk\", \"Mount\", \"Used\", \"Free\", \"Total\", \"Used%\", \"R/s\", \"W/s\"]\n```\n\n## Filtering Entries\n\nYou can filter out what entries to show by configuring `[disk.name_filter]` and `[disk.mount_filter]` to filter by name and mount point respectively. In particular,\nyou can set a list of things to filter with by setting `list`, and configure how that list\nis processed with the other options.\n\nFor example, consider a disk widget showing these entries:\n\n![Disk no filter](../../assets/screenshots/config/disk-filtering/disk_no_filter.webp)\n\nIf we wanted to ignoring any entry with a name that matches `/dev/sda`:\n\n```toml\n[disk.name_filter]\n# Whether to ignore any matches. Defaults to true.\nis_list_ignored = true\n\n# A list of filters to try and match.\nlist = [\"/dev/sda\"]\n\n# Whether to use regex. Defaults to false.\nregex = true\n\n# Whether to be case-sensitive. Defaults to false.\ncase_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\nwhole_word = false\n```\n\nThis would give us:\n\n![Disk widget with just disk name filter](../../assets/screenshots/config/disk-filtering/disk_name_filter.webp)\n\nWe can also combine both the name filter and mount filter. For example:\n\n```toml\n[disk.name_filter]\nis_list_ignored = false\nlist = [\"/dev/sda\"]\nregex = true\ncase_sensitive = false\nwhole_word = false\n\n[disk.mount_filter]\nis_list_ignored = true\nlist = [\"/mnt/.*\", \"/\"]\nregex = true\ncase_sensitive = false\nwhole_word = true\n```\n\nThis gives us:\n\n![Disk widget with disk name and mount filter](../../assets/screenshots/config/disk-filtering/disk_name_mount_filter.webp)\n"
  },
  {
    "path": "docs/content/configuration/config-file/flags.md",
    "content": "# Flags\n\n!!! Warning\n\n    This section is in progress, and is just copied from the old documentation.\n\nYou can configure flags by putting them in `[flags]` table. Example:\n\n```toml\n[flags]\nhide_avg_cpu = true\n```\n\nMost of the [command line flags](../command-line-options.md) have config file equivalents to avoid having to type them out\neach time:\n\n| Field                        | Type                                                                                                               | Functionality                                                                                                                |\n| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |\n| `hide_avg_cpu`               | Boolean                                                                                                            | Hides the average CPU usage.                                                                                                 |\n| `dot_marker`                 | Boolean                                                                                                            | Uses a dot marker for graphs.                                                                                                |\n| `cpu_left_legend`            | Boolean                                                                                                            | Puts the CPU chart legend to the left side.                                                                                  |\n| `current_usage`              | Boolean                                                                                                            | Sets process CPU% to be based on current CPU%.                                                                               |\n| `group_processes`            | Boolean                                                                                                            | Groups processes with the same name by default.                                                                              |\n| `case_sensitive`             | Boolean                                                                                                            | Enables case sensitivity by default.                                                                                         |\n| `whole_word`                 | Boolean                                                                                                            | Enables whole-word matching by default.                                                                                      |\n| `regex`                      | Boolean                                                                                                            | Enables regex by default.                                                                                                    |\n| `basic`                      | Boolean                                                                                                            | Hides graphs and uses a more basic look.                                                                                     |\n| `use_old_network_legend`     | Boolean                                                                                                            | DEPRECATED - uses the older network legend.                                                                                  |\n| `battery`                    | Boolean                                                                                                            | Shows the battery widget.                                                                                                    |\n| `rate`                       | Unsigned Int (represents milliseconds) or String (represents human time)                                           | Sets a refresh rate in ms.                                                                                                   |\n| `default_time_value`         | Unsigned Int (represents milliseconds) or String (represents human time)                                           | Default time value for graphs in ms.                                                                                         |\n| `time_delta`                 | Unsigned Int (represents milliseconds) or String (represents human time)                                           | The amount in ms changed upon zooming.                                                                                       |\n| `hide_time`                  | Boolean                                                                                                            | Hides the time scale.                                                                                                        |\n| `temperature_type`           | String (one of [\"k\", \"f\", \"c\", \"kelvin\", \"fahrenheit\", \"celsius\"])                                                 | Sets the temperature unit type.                                                                                              |\n| `default_widget_type`        | String (one of [\"cpu\", \"proc\", \"net\", \"temp\", \"mem\", \"disk\"], same as layout options)                              | Sets the default widget type, use --help for more info.                                                                      |\n| `default_widget_count`       | Unsigned Int (represents which `default_widget_type`)                                                              | Sets the n'th selected widget type as the default.                                                                           |\n| `disable_click`              | Boolean                                                                                                            | Disables mouse clicks.                                                                                                       |\n| `enable_cache_memory`        | Boolean                                                                                                            | Enable cache and buffer memory stats (not available on Windows).                                                             |\n| `process_memory_as_value`    | Boolean                                                                                                            | Defaults to showing process memory usage by value.                                                                           |\n| `tree`                       | Boolean                                                                                                            | Defaults to showing the process widget in tree mode.                                                                         |\n| `show_table_scroll_position` | Boolean                                                                                                            | Shows the scroll position tracker in table widgets.                                                                          |\n| `process_command`            | Boolean                                                                                                            | Show processes as their commands by default.                                                                                 |\n| `disable_advanced_kill`      | Boolean                                                                                                            | Disable being able to send signals to processes on supported Unix-like systems. Only available on Linux, macOS, and FreeBSD. |\n| `read_only`                  | Boolean                                                                                                            | Prevents performing any actions that affect the system (e.g. stopping processes).                                           |\n| `network_use_binary_prefix`  | Boolean                                                                                                            | Displays the network widget with binary prefixes.                                                                            |\n| `network_use_bytes`          | Boolean                                                                                                            | Displays the network widget using bytes.                                                                                     |\n| `network_use_log`            | Boolean                                                                                                            | Displays the network widget with a log scale.                                                                                |\n| `disable_gpu`                | Boolean                                                                                                            | Disable NVIDIA and AMD GPU data collection.                                                                                  |\n| `retention`                  | String (human readable time, such as \"10m\", \"1h\", etc.)                                                            | How much data is stored at once in terms of time.                                                                            |\n| `unnormalized_cpu`           | Boolean                                                                                                            | Show process CPU% without normalizing over the number of cores.                                                              |\n| `expanded`                   | Boolean                                                                                                            | Expand the default widget upon starting the app.                                                                             |\n| `memory_legend`              | String (one of [\"none\", \"top-left\", \"top\", \"top-right\", \"left\", \"right\", \"bottom-left\", \"bottom\", \"bottom-right\"]) | Where to place the legend for the memory widget.                                                                             |\n| `network_legend`             | String (one of [\"none\", \"top-left\", \"top\", \"top-right\", \"left\", \"right\", \"bottom-left\", \"bottom\", \"bottom-right\"]) | Where to place the legend for the network widget.                                                                            |\n| `average_cpu_row`            | Boolean                                                                                                            | Moves the average CPU usage entry to its own row when using basic mode.                                                      |\n| `tree_collapse`              | Boolean                                                                                                            | Collapse process tree by default.                                                                                            |\n| `hide_k_threads`             | Boolean                                                                                                            | Hide kernel threads by default.                                                                                              |\n| `free_arc`                   | Boolean                                                                                                            | Subtract freeable ARC from memory.                                                                                           |\n"
  },
  {
    "path": "docs/content/configuration/config-file/index.md",
    "content": "# Config File\n\nFor persistent configuration, and for certain configuration options, bottom supports config files.\n\n## Default Config File\n\nIf no config file argument is given, it will automatically look for a config file at these locations:\n\n| OS      | Default Config Location                                                                                                                    |\n| ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |\n| macOS   | `$HOME/Library/Application Support/bottom/bottom.toml`<br/> `$HOME/.config/bottom/bottom.toml` <br/> `$XDG_CONFIG_HOME/bottom/bottom.toml` |\n| Linux   | `$HOME/.config/bottom/bottom.toml` <br/> `$XDG_CONFIG_HOME/bottom/bottom.toml`                                                             |\n| Windows | `C:\\Users\\<USER>\\AppData\\Roaming\\bottom\\bottom.toml`                                                                                       |\n\nIf the config file doesn't exist at the path, bottom will automatically try to create a new config file at the location\nwith default values.\n\n## JSON Schema\n\nThe configuration file also has [JSON Schema](https://json-schema.org/) support to make it easier to manage, if your\nIDE/editor supports it.\n"
  },
  {
    "path": "docs/content/configuration/config-file/layout.md",
    "content": "# Layout\n\n!!! Warning\n\n    This section is in progress, and is just copied from the old documentation.\n\nbottom supports customizable layouts via the config file. Currently, layouts are controlled by using TOML objects and arrays.\n\nFor example, given the sample layout:\n\n```toml\n[[row]]\n  [[row.child]]\n  type=\"cpu\"\n[[row]]\n    ratio=2\n    [[row.child]]\n      ratio=4\n      type=\"mem\"\n    [[row.child]]\n      ratio=3\n      [[row.child.child]]\n        type=\"temp\"\n      [[row.child.child]]\n        type=\"disk\"\n```\n\nThis would give a layout that has two rows, with a 1:2 ratio. The first row has only the CPU widget.\nThe second row is split into two columns with a 4:3 ratio. The first column contains the memory widget.\nThe second column is split into two rows with a 1:1 ratio. The first is the temperature widget, the second is the disk widget.\n\nThis is what the layout would look like when run:\n\n![Sample layout](../../assets/screenshots/config/layout/sample_layout.webp)\n\nEach `[[row]]` represents a _row_ in the layout. A row can have any number of `child` values. Each `[[row.child]]`\nrepresents either a _column or a widget_. A column can have any number of `child` values as well. Each `[[row.child.child]]`\nrepresents a _widget_. A widget is represented by having a `type` field set to a string.\n\nThe following `type` values are supported:\n\n|                                  |                          |\n| -------------------------------- | ------------------------ |\n| `\"cpu\"`                          | CPU chart and legend     |\n| `\"mem\", \"memory\"`                | Memory chart             |\n| `\"net\", \"network\"`               | Network chart and legend |\n| `\"proc\", \"process\", \"processes\"` | Process table and search |\n| `\"temp\", \"temperature\"`          | Temperature table        |\n| `\"disk\"`                         | Disk table               |\n| `\"empty\"`                        | An empty space           |\n| `\"batt\", \"battery\"`              | Battery statistics       |\n\nEach component of the layout accepts a `ratio` value. If this is not set, it defaults to 1.\n\nFurthermore, you can have duplicate widgets.\n\nFor an example, look at the [default config](https://github.com/ClementTsang/bottom/blob/main/sample_configs/default_config.toml), which contains the default layout.\n"
  },
  {
    "path": "docs/content/configuration/config-file/network.md",
    "content": "# Network\n\n## Filtering Entries\n\nYou can filter out what entries to show by configuring `[network.interface_filter]` .\nIn particular, you can set a list of things to filter with by setting `list`, and configure how that list is processed with the other options.\n\nFor example, here we are ignoring any entry with a name that matches `/dev/sda<NUMBERS>`, or specifically `/dev/nvme0n1p2`.\n\n```toml\n[network.interface_filter]\n# Whether to ignore any matches. Defaults to true.\nis_list_ignored = true\n\n# A list of filters to try and match.\nlist = [\"virbr0.*\"]\n\n# Whether to use regex. Defaults to false.\nregex = true\n\n# Whether to be case-sensitive. Defaults to false.\ncase_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\nwhole_word = false\n```\n"
  },
  {
    "path": "docs/content/configuration/config-file/processes.md",
    "content": "# Processes\n\n## Columns\n\nYou can configure which columns are shown by the process widget by setting the `columns` setting:\n\n```toml\n[processes]\n# Pick which columns you want to use in any order.\ncolumns = [\"cpu%\", \"mem%\", \"pid\", \"name\", \"read\", \"write\", \"tread\", \"twrite\", \"state\", \"user\", \"time\", \"gmem%\", \"gpu%\"]\n```\n"
  },
  {
    "path": "docs/content/configuration/config-file/styling.md",
    "content": "# Styling\n\nVarious parts of the bottom can be styled, using either built-in themes or custom theming.\n\n## Precedence\n\nAs there are a few ways styles can be applied to bottom, the order of which styles are prioritized are, in order of\nhighest precedence to lowest precedence:\n\n1. Built-in themes set via command-line args (e.g. `btm --theme gruvbox`)\n2. Custom themes set via config file\n3. Built-in themes set via config file\n\nIf nothing is set, it will fall back to the default theme.\n\n## Built-in styles\n\nbottom has a few built-in themes:\n\n- Default\n- [Nord](https://www.nordtheme.com/)\n- [Gruvbox](https://github.com/morhetz/gruvbox)\n\nThese themes all also have light variants to support terminals using lighter colours.\n\nTo set the theme from the command line:\n\n```bash\nbtm --theme gruvbox\n```\n\nTo set the theme using the config file:\n\n```toml\n[styles]\ntheme = \"gruvbox\"\n```\n\n## Custom styling\n\nbottom's components can also be individually styled by the user to control the colour of the text style.\n\n### Colours\n\nYou can configure the colours for components with strings that are either hex colours (e.g. `\"#ffffff\"`), RGB colours\n(e.g. `\"255, 255, 255\"`), or named colours. Named colours are one of the following strings:\n\n- `\"Black\"`\n- `\"Red\"`\n- `\"Green\"`\n- `\"Yellow\"`\n- `\"Blue\"`\n- `\"Magenta\"`\n- `\"Cyan\"`\n- `\"Gray\"`\n- `\"DarkGray\"`\n- `\"LightRed\"`\n- `\"LightGreen\"`\n- `\"LightYellow\"`\n- `\"LightBlue\"`\n- `\"LightMagenta\"`\n- `\"LightCyan\"`\n- `\"White\"`\n\n### Text\n\nText can generally be styled using the following TOML table:\n\n```toml\n[field]\n# The foreground colour of text.\ncolor = \"black\"\n\n# The background colour of text.\nbg_color = \"blue\"\n\n# Whether to make the text bold.\nbold = false\n\n# Inline table version\nfield = { color = \"black\", bg_color = \"blue\", bold = false }\n```\n\nAll fields are optional; by default if `bg_color` is not set then there will be no background color.\n\nIf you _just_ want to style text by setting the foreground colour, for brevity, then you can also just set the field\nto be the colour itself. For example:\n\n```toml\n[styles.widgets]\nselected_text = \"#fff\"\n```\n\n### Configuration\n\n#### CPU\n\nThese can be set under `[styles.cpu]`:\n\n| Config field      | Details                                                          | Examples                                     |\n| ----------------- | ---------------------------------------------------------------- | -------------------------------------------- |\n| `all_entry_color` | The colour of the \"All\" CPU label                                | `all_entry_color = \"Red\"`                    |\n| `avg_entry_color` | The colour of the average CPU label and graph line               | `avg_entry_color = \"255, 0, 255\"`            |\n| `cpu_core_colors` | Colour of each CPU threads' label and graph line. Read in order. | `cpu_core_colors = [\"Red\", \"Blue\", \"Green\"]` |\n\n#### Memory\n\nThese can be set under `[styles.memory]`:\n\n| Config field  | Details                                                                        | Examples                                |\n| ------------- | ------------------------------------------------------------------------------ | --------------------------------------- |\n| `ram_color`   | The colour of the RAM label and graph line                                     | `ram_color = \"Red\"`                     |\n| `cache_color` | The colour of the cache label and graph line. Does not do anything on Windows. | `cache_color = \"#ffffff\"`               |\n| `swap_color`  | The colour of the swap label and graph line                                    | `swap_color = \"255, 0, 255\"`            |\n| `arc_color`   | The colour of the ARC label and graph line                                     | `arc_color = \"Blue\"`                    |\n| `gpu_colors`  | Colour of each GPU's memory label and graph line. Read in order.               | `gpu_colors = [\"Red\", \"Blue\", \"Green\"]` |\n\n#### Network\n\nThese can be set under `[styles.network]`:\n\n| Config field     | Details                                                   | Examples                     |\n| ---------------- | --------------------------------------------------------- | ---------------------------- |\n| `rx_color`       | The colour of the RX (download) label and graph line      | `rx_color = \"Red\"`           |\n| `tx_color`       | The colour of the TX (upload) label and graph line        | `tx_color = \"#ffffff\"`       |\n| `rx_total_color` | The colour of the total RX (download) label in basic mode | `rx_total_color = \"0, 0, 0\"` |\n| `tx_total_color` | The colour of the total TX (upload) label in basic mode   | `tx_total_color = \"#000\"`    |\n\n#### Battery\n\nThese can be set under `[styles.battery]`:\n\n| Config field           | Details                                                                  | Examples                           |\n| ---------------------- | ------------------------------------------------------------------------ | ---------------------------------- |\n| `high_battery_color`   | The colour of the battery widget bar when the battery is over 50%        | `high_battery_color = \"Red\"`       |\n| `medium_battery_color` | The colour of the battery widget bar when the battery between 10% to 50% | `medium_battery_color = \"#ffffff\"` |\n| `low_battery_color`    | The colour of the battery widget bar when the battery is under 10%       | `low_battery_color = \"0, 0, 0\"`    |\n\n#### Tables\n\nThese can be set under `[styles.tables]`:\n\n| Config field | Details                        | Examples                                                       |\n| ------------ | ------------------------------ | -------------------------------------------------------------- |\n| `headers`    | Text styling for table headers | `headers = { color = \"red\", bg_color = \"black\", bold = true }` |\n\n#### Graphs\n\nThese can be set under `[styles.graphs]`:\n\n| Config field  | Details                                      | Examples                                                            |\n| ------------- | -------------------------------------------- | ------------------------------------------------------------------- |\n| `graph_color` | The general colour of the parts of the graph | `graph_color = \"white\"`                                             |\n| `legend_text` | Text styling for graph's legend text         | `legend_text = { color = \"black\", bg_color = \"blue\", bold = true }` |\n\n#### General widget settings\n\nThese can be set under `[styles.widgets]`:\n\n| Config field            | Details                                                                                      | Examples                                                              |\n| ----------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |\n| `border_color`          | The colour of the widgets' borders                                                           | `border_color = \"white\"`                                              |\n| `selected_border_color` | The colour of a widget's borders when the widget is selected                                 | `selected_border_color = \"white\"`                                     |\n| `widget_title`          | Text styling for a widget's title                                                            | `widget_title = { color = \"black\", bg_color = \"blue\", bold = true }`  |\n| `text`                  | Text styling for text in general                                                             | `text = { color = \"black\", bg_color = \"blue\", bold = true }`          |\n| `selected_text`         | Text styling for text when representing something that is selected                           | `selected_text = { color = \"black\", bg_color = \"blue\", bold = true }` |\n| `disabled_text`         | Text styling for text when representing something that is disabled                           | `disabled_text = { color = \"black\", bg_color = \"blue\", bold = true }` |\n| `thread_text`           | Text styling for text when representing process threads. Only usable on Linux at the moment. | `thread_text = { color = \"green\", bg_color = \"blue\", bold = true }`   |\n"
  },
  {
    "path": "docs/content/configuration/config-file/temperature-table.md",
    "content": "# Temperature Table\n\n## Filtering Entries\n\nYou can filter out what entries to show by configuring `[temperature.sensor_filter]`. In particular you can set a list of things to filter with by setting `list`, and configure how that list is processed with the other options.\n\nFor example, here we are ignoring any sensor that has \"cpu\" or \"wifi\" in it.\n\n```toml\n[temperature.sensor_filter]\n# Whether to ignore any matches. Defaults to true.\nis_list_ignored = true\n\n# A list of filters to try and match.\nlist = [\"cpu\", \"wifi\"]\n\n# Whether to use regex. Defaults to false.\nregex = false\n\n# Whether to be case-sensitive. Defaults to false.\ncase_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\nwhole_word = false\n```\n"
  },
  {
    "path": "docs/content/contribution/development/build_process.md",
    "content": "# Build Process\n\n!!! Warning\n\n    This section is currently somewhat WIP.\n\n!!! Warning\n\n    This section is intended for people who wish to work on/build/distribute bottom, not general users.\n\n## Overview\n\nbottom manages its own binary builds for nightly and stable release purposes. The core build workflow is handled by [`build_releases.yml`](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/build_releases.yml), called by a wrapper workflow for [nightly](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/nightly.yml) and [stable](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/deployment.yml) releases. Builds take place via GitHub Actions.\n\nThe main things built are:\n\n- Binaries for various platforms\n- MSI installer for Windows\n- `.deb` package for Debian and its derivatives\n\nThis documentation gives a high-level overview of the build process for each part. For the most up-to-date and detailed reference, definitely refer back to the [`build_releases.yml`](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/build_releases.yml) file.\n\n## Binaries\n\nBinaries are built currently for various targets. Note that not all of these are officially supported. The following general steps are performed:\n\n- Set up the Rust toolchain for the action runner.\n- Enable cache.\n- Build a release build with:\n\n  - `--features deploy`, which enables only crates needed for release builds.\n  - `--locked` to lock the dependency versions.\n  - The following env variables set:\n\n    - `BTM_GENERATE: true`\n    - `COMPLETION_DIR: \"target/tmp/bottom/completion/\"`\n    - `MANPAGE_DIR: \"target/tmp/bottom/manpage/\"`\n\n    These generate the manpages and shell completions (see [Packaging](../packaging-and-distribution.md) for some more information).\n\n- Bundle the binaries and manpage/completions.\n- Cleanup.\n\nSome builds use [`cross`](https://github.com/cross-rs/cross) to do cross-compilation builds for architectures otherwise not natively supported by the runner.\n\n## MSI\n\nThis builds a full Windows installer using [`cargo-wix`](https://github.com/volks73/cargo-wix). This requires some setup beforehand with some dependencies:\n\n- Net-Framework-Core (handled by Powershell)\n- wixtoolset (handled by chocolatey)\n- Rust toolchain\n\nAfter that, cache is enabled, and `cargo wix` takes care of the rest.\n\n## `.deb`\n\nCurrently, `.deb` files are built for x86 and ARM architectures (`armv7`, `aarch64`). This is handled by [`cargo-deb`](https://crates.io/crates/cargo-deb).\n\n- For x86, this is handled natively with just `cargo-deb`.\n- For ARM, this uses a Docker container, [cargo-deb-arm](https://github.com/ClementTsang/cargo-deb-arm), which correctly sets the dependencies and architecture for the generated `.deb` file.\n\nThere are additional checks via `dpkg` to ensure the architecture is correctly set.\n"
  },
  {
    "path": "docs/content/contribution/development/deploy_process.md",
    "content": "# Deploy Process\n\n!!! Warning\n\n    This section is currently WIP.\n\n!!! Warning\n\n    This section is intended for people who wish to work on/build/distribute bottom, not general users.\n\n## Overview\n\nbottom currently has two main deploy processes to worry about:\n\n- [Nightly](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/nightly.yml): a daily (00:00 UTC) GitHub action to build binary/installer files, and upload them to the nightly release ([example](https://bottom.pages.dev/nightly/nightly-release)). It can also be triggered manually as either a proper nightly release or a mock release.\n- [Stable](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/deployment.yml): a stable deployment, triggered manually or upon creation of a valid tag. This is a GitHub action that builds binary/installer files and uploads them to a new GitHub release.\n\n  Furthermore, this workflow does not handle the following deployments, which must be manually handled:\n\n  - [Chocolatey](https://community.chocolatey.org/packages/bottom)\n  - [crates.io](https://crates.io/crates/bottom)\n\n## Nightly\n\nThis is, for the most part, automatic, though it can also be used as a way of testing build workflow changes and seeing if binaries can be successfully built at all against all the targets we want to build for.\n\nIf one does not want to actually update the nightly release, and just want to test the general builds and workflow, one can run the workflow manually on a branch of choice with \"mock\" set as the parameter. Changing it to anything else will trigger a non-mock run.\n\n## Stable\n\nThis can be manually triggered, though the general use-case is setting a tag of the form `x.y.z` (after checking everything is good, of course). For example:\n\n```bash\ngit tag 0.6.9 && git push origin 0.6.9\n```\n\nThis will automatically trigger the deployment workflow, and create a draft release with the files uploaded. One still needs to fill in the details and release it.\n\nFurthermore, there are some deployments that are handled by maintainers of bottom that this workflow does not automatically finish. These must be manually handled.\n\n### crates.io\n\nValidate everything builds properly and works (you should have done this before releasing though). If good, then deploying on crates.io is as simple as:\n\n```bash\ncargo publish\n```\n\n### Chocolatey\n\nUpon releasing on GitHub, [choco-bottom](https://github.com/ClementTsang/choco-bottom) will automatically be updated with a new PR with the correct deployment files for Chocolatey. Check the PR, merge it if it is correct, then pull locally and deploy following the instructions in the [README](https://github.com/ClementTsang/choco-bottom/blob/master/README.md). Make sure to test installation and running at least once before deploying!\n\nIf done correctly, there should be a new build on Chocolatey, which will take some time to validate.\n\n### winget\n\nSometimes, people will kindly do it, but it can also be manually triggered using\n[winget-bottom](https://github.com/ClementTsang/winget-bottom).\n\nNote this requires regenerating some secrets.\n"
  },
  {
    "path": "docs/content/contribution/development/dev_env.md",
    "content": "# Development Environment\n\n!!! Warning\n\n    This section is currently WIP.\n\n!!! Warning\n\n    This section is intended for people who wish to work on/build/distribute bottom, not general users.\n"
  },
  {
    "path": "docs/content/contribution/development/logging.md",
    "content": "# Logging\n\n!!! Warning\n\n    This section is currently WIP.\n\n!!! Warning\n\n    This section is intended for people who wish to work on/build/distribute bottom, not general users.\n"
  },
  {
    "path": "docs/content/contribution/development/testing.md",
    "content": "# Testing\n\n!!! Warning\n\n    This section is currently WIP.\n\n!!! Warning\n\n    This section is intended for people who wish to work on/build/distribute bottom, not general users.\n"
  },
  {
    "path": "docs/content/contribution/documentation.md",
    "content": "# Documentation\n\n## When should documentation changes be done?\n\n- Whenever a new feature is added, a bug is fixed, or a breaking change is made, it should be documented where\n  appropriate (ex: `README.md`, changelog, etc.)\n- New methods of installation are always appreciated and should be documented\n\n## What pages need documentation?\n\nThere are a few areas where documentation changes are often needed:\n\n- The [`README.md`](https://github.com/ClementTsang/bottom/blob/main/README.md)\n- The help menu inside of the application (located [here](https://github.com/ClementTsang/bottom/blob/main/src/constants.rs))\n- The [extended documentation](../index.md) (what you're reading right now)\n- The [`CHANGELOG.md`](https://github.com/ClementTsang/bottom/blob/main/CHANGELOG.md)\n\n## How should I add/update documentation?\n\n1. Fork the repository to make changes in.\n\n2. Where you're adding documentation will probably affect what you need to do:\n\n   <h3><code>README.md</code> or <code>CHANGELOG.md</code></h3>\n\n   For changes to [`README.md`](https://github.com/ClementTsang/bottom/blob/main/README.md) and [`CHANGELOG.md`](https://github.com/ClementTsang/bottom/blob/main/CHANGELOG.md), just follow the formatting provided and use any editor.\n\n   Generally, changes to [`CHANGELOG.md`](https://github.com/ClementTsang/bottom/blob/main/CHANGELOG.md) will be handled\n   by a maintainer, and the contents of the file should follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)\n   format, as well as link to the relevant PR or issues.\n\n   <h3>Help menu</h3>\n\n   For changes to the help menu, try to refer to the existing code within [`src/constants.rs`](https://github.com/ClementTsang/bottom/blob/main/src/constants.rs) on how the help menu is generated.\n\n   <h3>Extended documentation</h3>\n\n   For changes to the extended documentation, you'll probably want at least Python 3.11 (older and newer versions\n   should be fine), [MkDocs](https://www.mkdocs.org/), [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),\n   `mdx_truly_sane_lists`, and optionally [Mike](https://github.com/jimporter/mike) installed. These can help with\n   validating your changes locally.\n\n   You can do so through `pip` or your system's package managers. If you use `pip`, you can use venv to cleanly install\n   the documentation dependencies:\n\n   ```bash\n   # Change directories to the documentation.\n   cd docs/\n\n    # Create venv, install the dependencies, and serve the page.\n   ./serve.sh\n   ```\n\n   This will serve a local version of the docs that you can open on your browser. It will update as you make changes.\n\n3. Once you have your documentation changes done, submit it as a pull request. For more information regarding that,\n   refer to [Issues, Pull Requests, and Discussions](issues-and-pull-requests.md).\n"
  },
  {
    "path": "docs/content/contribution/issues-and-pull-requests.md",
    "content": "# Issues, Pull Requests, and Discussions\n\n## Discussions\n\nDiscussions are open [in the repo](https://github.com/ClementTsang/bottom/discussions). As for the difference between discussions and issues:\n\n- Open an issue if what you have enough information to properly fill out any details needed for a report or request.\n- Open a discussion otherwise (e.g. asking a question).\n\n## Opening an issue\n\n### Bug reports\n\nWhen filing a bug report, please use the [bug report template](https://github.com/ClementTsang/bottom/issues/new?assignees=&labels=bug&template=bug_report.md&title=) and fill in as much as you can. It is _incredibly_ difficult for a maintainer to fix a bug when it cannot be reproduced, and giving as much detail as possible generally helps to make it easier to reproduce the problem!\n\n### Feature requests\n\nPlease use the [feature request template](https://github.com/ClementTsang/bottom/issues/new?assignees=&labels=feature&template=feature_request.md&title=) and fill it out. Remember to give details about what the feature is along with why you think this suggestion will be useful.\n\nAlso, please check whether an existing issue has covered your specific feature request!\n\n## Pull requests\n\nThe expected workflow for a pull request is:\n\n1. Fork the project.\n2. Make your changes.\n3. Make any documentation changes if necessary - if you add a new feature, it'll probably need documentation changes. See [here](./documentation.md) for tips on documentation.\n4. Commit and create a pull request to merge into the `main` branch. **Please fill out the pull request template**.\n5. Ask a maintainer to review your pull request.\n   - Check if the CI workflow passes. These consist of clippy lints, rustfmt checks, and basic tests. If you are a\n     first-time contributor, you may need to wait for a maintainer to let CI run.\n   - If changes are suggested or any comments are made, they should probably be addressed.\n6. Once it looks good, it'll be merged! Note that _generally_, PRs are squashed to maintain repo cleanliness, though\n   feel free to ask otherwise if that isn't preferable.\n"
  },
  {
    "path": "docs/content/contribution/packaging-and-distribution.md",
    "content": "# Packaging and Distribution\n\nPackage maintainers are always welcome and appreciated! Here's some info on how one can help with package distribution\nand bottom.\n\n## Pre-built binaries\n\nThe latest stable release can be found [here](https://github.com/ClementTsang/bottom/releases/latest), where you can\nfind pre-built binaries in either a `tar.gz` or `zip` format. Binaries here also include automatically generated shell\ncompletion files for zsh, bash, fish, and Powershell, which you may want to also install during the packaging\nprocess.\n\nYou can also find a nightly build in the [releases page](https://github.com/ClementTsang/bottom/releases), built every\nday at 00:00 UTC off of the `main` branch.\n\nIn both cases, we use a combination of GitHub Actions and CirrusCI (mainly for FreeBSD and macOS M1) to create our\nrelease binaries. [`build_releases.yml`](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/build_releases.yml)\ncontains the GitHub Action workflow used to do both of these, if reference is needed.\n\n## Building manually\n\nIf you want to manually build bottom rather than distributing a pre-built binary, you'll need the most recent version\nof stable Rust, which you can get with:\n\n```bash\nrustup update stable\n```\n\nYou'll then want to build with:\n\n```bash\ncargo build --release --locked\n```\n\n### Manpage and completion generation\n\nbottom uses a [`build.rs`](https://github.com/ClementTsang/bottom/blob/main/build.rs) script to automatically generate\na manpage and shell completions for the following shells:\n\n- Bash\n- Zsh\n- Fish\n- Powershell\n- Elvish\n\nIf you want to generate manpages and/or completion files, set the `BTM_GENERATE` env var to a non-empty value. For\nexample, run something like this:\n\n```bash\nBTM_GENERATE=true cargo build --release --locked\n```\n\nThis will automatically generate completion and manpage files in `target/tmp/bottom/`. If you wish to regenerate the\nfiles, modify/delete either these files or set `BTM_GENERATE` to some other non-empty value to retrigger the build\nscript.\n\nYou may override the default directories used to generate both completion and manpage files by specifying the\n`COMPLETION_DIR` and `MANPAGE_DIR` environment variables respectively.\n\nFor more information, you may want to look at either the [`build.rs`](https://github.com/ClementTsang/bottom/blob/main/build.rs)\nfile or the [binary build CI workflow](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/build_releases.yml).\n\n## Adding an installation source\n\nOnce you've finished your installation source, if you want to mention it in the main bottom repo, fork the repo and add\nthe installation method and any details to the [`README.md`](https://github.com/ClementTsang/bottom/blob/main/README.md)\nfile under the [Installation](https://github.com/ClementTsang/bottom#installation) section, as well as a corresponding\ntable of contents entry. Once that's done, open a pull request - these will usually be approved of very quickly.\n\nYou can find more info on the contribution process [here](issues-and-pull-requests.md#pull-requests).\n"
  },
  {
    "path": "docs/content/index.md",
    "content": "﻿---\nhide:\n  - navigation\n  - toc\ntitle: Home\n---\n\n# `bottom`\n\nA customizable cross-platform graphical process/system monitor for the terminal, supporting Linux, macOS, and Windows. Inspired by other tools like [gtop](https://github.com/aksakalli/gtop), [gotop](https://github.com/xxxserxxx/gotop), and [htop](https://github.com/htop-dev/htop).\n\n---\n\nThis site serves as extended documentation for bottom alongside the [`README.md`](https://github.com/ClementTsang/bottom#readme).\n\n!!! Warning\n\n    Some areas of this site are still in progress and may be missing details.  Feel free to suggest/contribute changes!\n\n## Installation\n\n!!! Tip\n\n    It's as good idea to first check out the [Support](support/official.md) page to see if your system is officially supported!\n\n!!! Tip\n\n    If you're facing some issues during/after installation, check out the [Troubleshooting](troubleshooting.md) page for some common problems and solutions.\n\nTo install bottom, refer to [the installation section of the `README.md`](https://github.com/ClementTsang/bottom#installation),\nwhich contains a list of all the installation methods.\n\n## Usage and configuration\n\nThe command to run bottom is `btm`.\n\nYou can refer to the [usage](usage/general-usage.md) pages for more details on using bottom (e.g. keybinds, some features, a general overview of what each widget does).\n\nTo configure bottom (e.g. how it behaves, how it looks, etc.) refer to the [command-line options page](configuration/command-line-options.md) for temporary settings, or [the config file page](configuration/config-file/index.md) for more permanent settings.\n\n## Contribution\n\nNew contributors are always welcome! See the [contribution](contribution/issues-and-pull-requests.md) section for how to contribute to\nbottom, whether it be filing issues, writing documentation, creating pull requests, etc.\n"
  },
  {
    "path": "docs/content/nightly-release.md",
    "content": "<!-- Intentionally empty file, used for redirects -->\n"
  },
  {
    "path": "docs/content/stylesheets/extra.css",
    "content": ":root {\n  --md-primary-fg-color: #268bd2;\n  --md-accent-fg-color: #81a1c1;\n}\n\n.md-typeset__table {\n  min-width: 100%;\n}\n\n.md-typeset table:not([class]) {\n  display: table;\n}\n"
  },
  {
    "path": "docs/content/support/official.md",
    "content": "# Official support\n\nbottom _officially_ supports the following operating systems and corresponding architectures:\n\n- macOS (`x86_64`, `aarch64`)\n- Linux (`x86_64`, `i686`, `aarch64`)\n- Windows (`x86_64`, `i686`)\n\nThese platforms are tested to work (with caveats, see below) and issues on these platforms will be fixed if possible.\n\nFurthermore, binaries are expected to be built and tested using the most recent version of stable Rust - if you are manually building\nbottom from the repo/source, then please try that as well.\n\nIf you encounter any issues, see the [troubleshooting page](../troubleshooting.md) or open an issue/discussion on GitHub.\n"
  },
  {
    "path": "docs/content/support/unofficial.md",
    "content": "# Unofficial support\n\nSystems and architectures that aren't officially supported may still work, but there are no guarantees on how much will\nwork. For example, it might only compile, or it might run with bugs/broken features. Furthermore, while it will depend\non the problem at the end of the day, _issues on unsupported platforms are likely to go unfixed_.\n\nUnofficially supported platforms known to compile/work:\n\n- FreeBSD\n- Linux on ARMv7 and ARMv6 (tested to compile in [CI](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/ci.yml))\n- Linux on PowerPC 64 LE (tested to compile in [CI](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/ci.yml))\n- Linux on an RISC-V (tested to compile in [CI](https://github.com/ClementTsang/bottom/blob/main/.github/workflows/ci.yml), tested to run on an [Allwinner D1 Nezha](https://github.com/ClementTsang/bottom/issues/564))\n\nIf you encounter any issues, see the [troubleshooting page](../troubleshooting.md) or open an issue/discussion on GitHub - though as mentioned, unsupported platforms may go unfixed.\n"
  },
  {
    "path": "docs/content/troubleshooting.md",
    "content": "# Troubleshooting/Known Issues\n\n## The graph points look broken/strange\n\nIt's possible that your graphs don't look great out of the box due to the reliance on\n[braille characters](https://en.wikipedia.org/wiki/Braille_Patterns) to draw them. This could cause problems if\nyour terminal's font does not support them, or your terminal is not configured properly to draw them.\n\n<figure>\n    <img src=\"../assets/screenshots/troubleshooting/no_braille.webp\" alt=\"Example of a terminal with no braille font.\"/>\n    <figcaption><sub>An example of missing braille fonts in Powershell</sub></figcaption>\n</figure>\n\nSome possible solutions are included below.\n\n### Use dot markers instead\n\nOne alternative is to use the `--dot_marker` option to render graph charts using dots instead of the braille characters,\nwhich generally seems better supported out of the box, at the expense of looking less intricate:\n\n<figure>\n    <img src=\"../assets/screenshots/troubleshooting/dots.webp\" alt=\"Example of running bottom with the dot marker flag\"/>\n    <figcaption><sub>Example using <code>btm --dot_marker</code></sub></figcaption>\n</figure>\n\n### Use a font that supports braille fonts\n\nAnother (better) alternative is to install a font that supports braille fonts, and configure your terminal emulator to\nuse it. For example, installing something like [UBraille](https://yudit.org/download/fonts/UBraille/) or\n[Iosevka](https://github.com/be5invis/Iosevka) and ensuring your terminal uses it should work.\n\n#### Linux/macOS/Unix\n\nSolutions mostly depend on what terminal emulator you are using, so unfortunately, I can't give specific instructions.\nHere are some possible solutions:\n\n- Uninstalling `gnu-free-fonts` if installed, as that is known to cause problems with braille markers\n- Installing a font like `ttf-symbola` or `ttf-ubraille` for your terminal emulator to try and automatically fall back to\n- Configuring your terminal emulator to use specific fonts for the `U+2800` to `U+28FF` range.\n  - For example for kitty, do `symbol_map U+2800-U+28FF Symbola`.\n\nFor some more possible solutions:\n\n- Check out [this issue](https://github.com/cjbassi/gotop/issues/18) from gotop about the same issue.\n- See ratatui's [FAQ](https://ratatui.rs/faq/#some-characters-appear-to-be-missing--look-weird) (ratatui is the underlying\n  library bottom uses to draw things).\n\n#### Windows and Powershell\n\n**Note: I would advise backing up your registry beforehand if you aren't sure what you are doing!**\n\nLet's say you're installing [Iosevka](https://github.com/be5invis/Iosevka). The steps you can take are:\n\n1. Install the font itself.\n2. Open the registry editor, which you can do either by `Win+R` and opening `regedit`, or just opening it from the Start Menu.\n3. In the registry editor, go to\n\n   ```\n   HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Console\\TrueTypeFont\n   ```\n\n4. Here, add a new `String value`, and set the `Name` to a bunch of 0's (e.g. `000` - make sure the name isn't already used), then set the `Data` to the font name (e.g. `Iosevka`).\n\n<figure>\n    <img src=\"../assets/screenshots/troubleshooting/regedit_fonts.webp\" alt=\"Regedit menu showing how to add a new font for Command Prompt/PowerShell\"/>\n    <figcaption><sub>The last entry is the new entry for Iosevka</sub></figcaption>\n</figure>\n\n5. Then, open the Command Prompt/PowerShell, and right-click on the top bar, and open \"Properties\":\n\n<figure>\n    <img src=\"../assets/screenshots/troubleshooting/cmd_prompt_props.webp\" alt=\"Opening the properties menu in Command Prompt/PowerShell\"/>\n</figure>\n\n6. From here, go to \"Font\", and set the font to your new font (so in this example, Iosevka):\n\n<figure>\n    <img src=\"../assets/screenshots/troubleshooting/cmd_prompt_font.webp\" alt=\"Setting a new font in Command Prompt/PowerShell\"/>\n</figure>\n\n## Why can't I see all my temperature sensors on Windows?\n\nThis is a known issue, some sensors may require admin privileges to get sensor data.\n\n## Why don't I see dual batteries on Windows reported separately? (e.g. Thinkpads)\n\nThis is a known issue which seems to be with how batteries are being detected on Windows.\n\n## Why can't I see all my temperature sensors on WSL?\n\nThis is a known limitation with WSL. Due to how it works, hosts may not expose their\ntemperature sensors and therefore, temperature sensors might be missing.\n\n## Why does WSL2 not match Task Manager?\n\nThis is a known limitation with WSL2. Due to how WSL2 works, the two might not match\nup in terms of reported data.\n\n## Why can't I see all my processes/process data on macOS?\n\nYou may have to run the program with elevated privileges to work around it - for example:\n\n```bash\nsudo btm\n```\n\n!!! Warning\n\n    Please note that you should be certain that you trust any software you grant root privileges.\n\n    There are measures taken to try to maximize the amount of information obtained without elevated privileges. For example,\n    one can modify the instructions found on the [htop wiki](https://github.com/hishamhm/htop/wiki/macOS:-run-without-sudo)\n    on how to run htop without sudo for bottom. However, **please** understand the potential security risks before doing so!\n\n## My configuration file isn't working\n\nIf your configuration files aren't working, here are a few things to try:\n\n### Check the formatting\n\nIt may be handy to refer to the automatically generated config files or the\n[sample configuration files](https://github.com/ClementTsang/bottom/tree/main/sample_configs). The config files also\nfollow the [TOML](https://toml.io/en/) format.\n\nAlso make sure your config options are under the right table - for example, to set your temperature type, you must\nset it under the `[flags]` table:\n\n```toml\n[flags]\ntemperature_type = \"f\"\n```\n\nMeanwhile, if you want to set a custom color scheme, it would be under the `[styles]` table:\n\n```toml\n[styles.tables.headers]\ncolor=\"LightBlue\"\n```\n\nTo help validate your configuration files, there is [JSON Schema](https://json-schema.org/) support if your IDE/editor\nsupports it.\n\n### Check the configuration file location\n\nMake sure bottom is reading the right configuration file. By default, bottom looks for config files at these locations:\n\n| OS      | Default Config Location                                                                                                                |\n| ------- | -------------------------------------------------------------------------------------------------------------------------------------- |\n| macOS   | `$HOME/Library/Application Support/bottom/bottom.toml`<br/> `~/.config/bottom/bottom.toml` <br/> `$XDG_CONFIG_HOME/bottom/bottom.toml` |\n| Linux   | `~/.config/bottom/bottom.toml` <br/> `$XDG_CONFIG_HOME/bottom/bottom.toml`                                                             |\n| Windows | `C:\\Users\\<USER>\\AppData\\Roaming\\bottom\\bottom.toml`                                                                                   |\n\nIf you want to use a config file in another location, use the `--config` or `-C` flags along with the path to the configuration file, like so:\n\n```bash\nbtm -C path_to_config\n```\n\n## My installation through snap has some widgets that are blank/show no data\n\nMake sure bottom is given the correct permissions in order to collect data. [Snapcraft](https://snapcraft.io/docs/interface-management)\nexplains how to do so, but the TL;DR is:\n\n```bash\nsudo snap connect bottom:mount-observe\nsudo snap connect bottom:hardware-observe\nsudo snap connect bottom:system-observe\nsudo snap connect bottom:process-control\n```\n\n## I don't see any NVIDIA GPU information while using a musl-based binary\n\nThe underlying interface we use for NVIDIA GPU information, nvml, only works with `glibc` and does not work with `musl` at the moment (see [this forum post](https://forums.developer.nvidia.com/t/provide-driver-for-muslc-to-install-it-in-musl-distros/219586/7) for some more details). As such, bottom may fail to get NVIDIA GPU information when using a musl-based binary until this is resolved. This applies to Linux and Windows from my understanding.\n\nTo resolve this, use `glibc`-based binary builds if possible (e.g. the `gnu` binaries/non-`musl` packages in [releases](https://github.com/ClementTsang/bottom/releases)).\n\n## Still having issues?\n\nIf you're still having issues, feel free to open a [discussion](https://github.com/ClementTsang/bottom/discussions/new/)\nquestion about it, and I (or others) can try to help.\n"
  },
  {
    "path": "docs/content/usage/autocomplete.md",
    "content": "# Auto-Complete\n\nThe release binaries in [the releases page](https://github.com/ClementTsang/bottom/releases) are packaged with\nshell auto-completion files for Bash, Zsh, fish, Powershell, Elvish, Fig, and Nushell. To install them:\n\n- For Bash, move `btm.bash` to `$XDG_CONFIG_HOME/bash_completion or /etc/bash_completion.d/`.\n- For Zsh, move `_btm` to one of your `$fpath` directories.\n- For fish, move `btm.fish` to `$HOME/.config/fish/completions/`.\n- For PowerShell, add `_btm.ps1` to your PowerShell [profile](<https://docs.microsoft.com/en-us/previous-versions//bb613488(v=vs.85)>).\n- For Elvish, the completion file is `btm.elv`.\n- For Fig, the completion file is `btm.ts`.\n- For Nushell, source `btm.nu`.\n\nThe individual auto-completion files are also included in the stable/nightly releases as `completion.tar.gz` if needed.\n"
  },
  {
    "path": "docs/content/usage/basic-mode.md",
    "content": "# Basic Mode\n\nBasic mode is a special layout that removes all of the graphs and provides an interface that resembles (a very stripped-down version of) htop.\n\n<figure>\n    <img src=\"../../assets/screenshots/basic.webp\" alt=\"A picture of bottom's basic mode.\"/>\n</figure>\n\nBasic mode can be enabled either through a command line flag:\n\n```bash\nbtm -b\n\n# or\n\nbtm --basic\n```\n\nor through the config:\n\n```toml\n[flags]\nbasic = true\n```\n\n## Notes\n\nIn this mode, widgets that use tables (temperatures, processes, disks, and batteries) are only shown one at a time.\nOne can switch between these widgets either by clicking the arrow buttons or by using the general widget selection shortcuts (for example, ++ctrl+left++ or ++H++)\nto switch which widget is shown.\n\nAlso note that in this mode, widget expansion and custom layouts are disabled.\n\n## Key bindings\n\nBasic mode follows the same key bindings as normal, barring widget expansion being disabled, and that the ++\"%\"++ key while selecting the memory widget toggles between total usage and percentage.\n"
  },
  {
    "path": "docs/content/usage/general-usage.md",
    "content": "# General Usage\n\nYou can run bottom with:\n\n```bash\nbtm\n```\n\nFor help regarding the command-line options, use:\n\n```bash\n# For a simple overview of flags\nbtm -h\n\n# For more details\nbtm --help\n```\n\nYou can also see keybinds and basic usage details in bottom by pressing ++question++, which will open a help menu.\n\n## Features\n\n### Expansion\n\nBy default, bottom is somewhat like a dashboard - a bunch of different widgets, all showing different things, and they all cram together to fit into one terminal.\n\nIf you instead just want to see _one_ widget - maybe you want to look at a graph in more detail, for example - you can \"expand\" the currently selected\nwidget using the ++e++ key, which will hide all other widgets and make that widget take up all available terminal space.\n\nYou can leave this state by either pressing ++e++ again or pressing ++esc++.\n\n### Widget selection\n\nTo allow for widget-specific keybindings and expansion, there is the idea of _widget selection_ in bottom, where you can focus on a specific widget to work with it.\nThis can be done with the mouse (just click on the widget of interest) or keyboard (ex: ++ctrl+\"Direction\"++, see [Key bindings](#key-bindings) for alternatives).\n\n## Key bindings\n\nThese are global or common keyboard shortcuts for the application, which you can see in-app through the ++question++ shortcut.\nNote that key bindings are generally case-sensitive.\n\n| Binding                                                      | Action                                                       |\n| ------------------------------------------------------------ | ------------------------------------------------------------ |\n| ++q++ , ++ctrl+c++                                           | Quit                                                         |\n| ++esc++                                                      | Close dialog windows, search, widgets, or exit expanded mode |\n| ++ctrl+r++                                                   | Reset display and any collected data                         |\n| ++f++                                                        | Freeze/unfreeze updating with new data                       |\n| ++question++                                                 | Open help menu                                               |\n| ++e++                                                        | Toggle expanding the currently selected widget               |\n| ++ctrl+up++ <br/> ++shift+up++ <br/> ++K++ <br/> ++W++       | Select the widget above                                      |\n| ++ctrl+down++ <br/> ++shift+down++ <br/> ++J++ <br/> ++S++   | Select the widget below                                      |\n| ++ctrl+left++ <br/> ++shift+left++ <br/> ++H++ <br/> ++A++   | Select the widget on the left                                |\n| ++ctrl+right++ <br/> ++shift+right++ <br/> ++L++ <br/> ++D++ | Select the widget on the right                               |\n| ++up++ , ++k++                                               | Move up within a widget                                      |\n| ++down++ , ++j++                                             | Move down within a widget                                    |\n| ++left++ <br/> ++h++ <br/> ++alt+h++                         | Move left within a widget                                    |\n| ++right++ <br/> ++l++ <br/> ++alt+l++                        | Move right within a widget                                   |\n| ++g+g++ , ++home++                                           | Jump to the first entry                                      |\n| ++G++ , ++end++                                              | Jump to the last entry                                       |\n| ++page-up++ , ++page-down++                                  | Scroll up/down a table by a page                             |\n| ++ctrl+u++                                                   | Scroll up a table by half a page                             |\n| ++ctrl+d++                                                   | Scroll down a table by half a page                           |\n\n## Mouse bindings\n\n| Binding     | Action             |\n| ----------- | ------------------ |\n| ++lbutton++ | Selects the widget |\n"
  },
  {
    "path": "docs/content/usage/widgets/battery.md",
    "content": "# Battery Widget\n\n!!! Warning\n\n    The battery features are unavailable if the binary is compiled with the `battery` feature disabled or if there are no batteries on the system!\n\nThe battery widget provides information about batteries on the system.\n\n<figure>\n    <img src=\"../../../assets/screenshots/battery.webp\" alt=\"A picture of an expanded battery widget.\"/>\n</figure>\n\nThe battery widget can be enabled through either the `--battery` flag, the `battery = true` option in a config file, or specifying the widget in a custom layout.\n\n## Features\n\nThe following data is displayed for batteries:\n\n- Charge percent\n- Consumption rate\n- Charging state\n- Time to empty/charge, based on the current state\n- Battery health percent\n\nThe battery widget also supports devices with multiple batteries, and you can switch between them using the keyboard or the mouse.\n\n## Key bindings\n\nNote that key bindings are generally case-sensitive.\n\n| Binding                               | Action                                                     |\n| ------------------------------------- | ---------------------------------------------------------- |\n| ++left++ <br/> ++h++ <br/> ++alt+h++  | Moves to the battery entry to the left of the current one  |\n| ++right++ <br/> ++l++ <br/> ++alt+l++ | Moves to the battery entry to the right of the current one |\n\n## Mouse bindings\n\n| Binding     | Action                  |\n| ----------- | ----------------------- |\n| ++lbutton++ | Selects a battery entry |\n"
  },
  {
    "path": "docs/content/usage/widgets/cpu.md",
    "content": "# CPU Widget\n\nThe CPU widget displays a visual representation of CPU usage over a time range.\n\n<figure>\n    <img src=\"../../../assets/screenshots/cpu.webp\" alt=\"A picture of an expanded CPU widget showing average CPU usage.\"/>\n</figure>\n\n## Features\n\nThe CPU widget is composed of two parts: the graph and the legend:\n\n- The graph displays the usage data for the currently selected entry as a percentage\n- The legend displays all available entries that can be displayed on the graph along with their last recorded use percentage (except for the \"All\" option)\n\nUsers can scroll through the legend using either the keyboard or mouse to select which entry to display on the graph. The \"All\" option shows every entry\nat the same time, though this may get a bit hard to follow if you have a large number of cores/threads.\n\nOne can also adjust the displayed time range through either the keyboard or mouse, with a range of 30s to 600s.\n\n## Key bindings\n\nNote that key bindings are generally case-sensitive.\n\n### Graph\n\n| Binding   | Action                                  |\n| --------- | --------------------------------------- |\n| ++plus++  | Zoom in on chart (decrease time range)  |\n| ++minus++ | Zoom out on chart (increase time range) |\n| ++equal++ | Reset zoom                              |\n\n### Legend\n\n| Binding            | Action                                |\n| ------------------ | ------------------------------------- |\n| ++up++ , ++k++     | Move up within a widget               |\n| ++down++ , ++j++   | Move down within a widget             |\n| ++g+g++ , ++home++ | Jump to the first entry in the legend |\n| ++G++ , ++end++    | Jump to the last entry in the legend  |\n\n## Mouse bindings\n\n### Graph\n\n| Binding      | Action                                                         |\n| ------------ | -------------------------------------------------------------- |\n| ++\"Scroll\"++ | Scrolling up or down zooms in or out of the graph respectively |\n\n### Legend\n\n| Binding      | Action                                            |\n| ------------ | ------------------------------------------------- |\n| ++\"Scroll\"++ | Scroll through options to display in the graph    |\n| ++lbutton++  | Selects a CPU thread/average to show in the graph |\n"
  },
  {
    "path": "docs/content/usage/widgets/disk.md",
    "content": "# Disk Widget\n\nThe disk widget provides a table of useful disk and partition information, like I/O per second and total usage.\n\n<figure>\n    <img src=\"../../../assets/screenshots/disk.webp\" alt=\"A picture of an expanded disk widget.\"/>\n</figure>\n\n## Features\n\nThe disk widget provides the following information:\n\n- Disk name\n- Disk mount location\n- Amount of space used\n- Amount of space left\n- Total amount of space\n- Percentage of space used\n- Read per second\n- Write per second\n\n## Key bindings\n\nNote that key bindings are generally case-sensitive.\n\n| Binding            | Action                                                              |\n| ------------------ | ------------------------------------------------------------------- |\n| ++up++ , ++k++     | Move up within a widget                                             |\n| ++down++ , ++j++   | Move down within a widget                                           |\n| ++g+g++ , ++home++ | Jump to the first entry in the table                                |\n| ++G++ , ++end++    | Jump to the last entry in the table                                 |\n| ++d++              | Sort by disk, press again to reverse sorting order                  |\n| ++m++              | Sort by mount, press again to reverse sorting order                 |\n| ++u++              | Sort by amount used, press again to reverse sorting order           |\n| ++n++              | Sort by amount free, press again to reverse sorting order           |\n| ++t++              | Sort by total space available, press again to reverse sorting order |\n| ++p++              | Sort by percentage used, press again to reverse sorting order       |\n| ++r++              | Sort by read rate, press again to reverse sorting order             |\n| ++w++              | Sort by write rate, press again to reverse sorting order            |\n\n## Mouse bindings\n\n| Binding     | Action                        |\n| ----------- | ----------------------------- |\n| ++lbutton++ | Selects an entry in the table |\n"
  },
  {
    "path": "docs/content/usage/widgets/memory.md",
    "content": "# Memory Widget\n\nThe memory widget provides a visual representation of RAM and swap usage over time.\n\n<figure>\n    <img src=\"../../../assets/screenshots/memory.webp\" alt=\"A picture of an expanded memory widget.\"/>\n</figure>\n\n## Features\n\nThe legend displays the current usage in terms of percentage and actual usage in binary units (KiB, MiB, GiB, etc.).\nIf the total RAM or swap available is 0, then it is automatically hidden from the legend and graph.\n\nOne can also adjust the displayed time range through either the keyboard or mouse, with a range of 30s to 600s.\n\nThis widget can also be configured to display Nvidia and AMD GPU memory usage (`--disable_gpu` on Linux/Windows to disable) or cache memory usage (`--enable_cache_memory`).\n\n## Key bindings\n\nNote that key bindings are generally case-sensitive.\n\n| Binding   | Action                                  |\n| --------- | --------------------------------------- |\n| ++plus++  | Zoom in on chart (decrease time range)  |\n| ++minus++ | Zoom out on chart (increase time range) |\n| ++equal++ | Reset zoom                              |\n\n## Mouse bindings\n\n| Binding      | Action                                                         |\n| ------------ | -------------------------------------------------------------- |\n| ++\"Scroll\"++ | Scrolling up or down zooms in or out of the graph respectively |\n\n## How are memory values determined?\n\n### Linux\n\nMemory usage is calculated using the following formula based on values from `/proc/meminfo` (based on [htop's implementation](https://github.com/htop-dev/htop/blob/976c6123f41492aaf613b9d172eef1842fb7b0a3/linux/LinuxProcessList.c#L1584)):\n\n```\nMemTotal - MemFree - Buffers - (Cached + SReclaimable - Shmem)\n```\n\nYou can find more info on `/proc/meminfo` and its fields [here](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/deployment_guide/s2-proc-meminfo).\n\n### Windows\n\nIn Windows, we calculate swap by querying `Get-Counter \"\\Paging File(*)\\% Usage\"`. This\nis also what some libraries like [psutil](https://github.com/giampaolo/psutil/blob/master/psutil/arch/windows/mem.c) use. However, note there are also a few other valid methods of\nrepresenting \"swap\" in Windows (e.g. using `GetPerformanceInfo`), which all slightly don't\nmatch.\n"
  },
  {
    "path": "docs/content/usage/widgets/network.md",
    "content": "# Network Widget\n\nThe network widget provides a visual representation of network input and output per second, as well as noting the total amount\nreceived and transmitted.\n\n<figure>\n    <img src=\"../../../assets/screenshots/network/network.webp\" alt=\"A picture of an expanded network widget.\"/>\n</figure>\n\n## Features\n\nThe legend displays the current reads and writes per second in bits, as well as the total amount read/written.\n\nThe y-axis automatically scales based on shown read/write values, and by default, is a linear scale based on base-10 units (e.x. kilobit, gigabit, etc.).\nThrough [configuration](../../configuration/command-line-options.md), the read/write per second unit can be changed to bytes, while the y-axis can be changed to a\nlog scale and/or use base-2 units (e.x. kibibit, gibibit, etc.).\n\nOne can also adjust the displayed time range through either the keyboard or mouse, with a range of 30s to 600s.\n\n## Key bindings\n\nNote that key bindings are generally case-sensitive.\n\n| Binding   | Action                                  |\n| --------- | --------------------------------------- |\n| ++plus++  | Zoom in on chart (decrease time range)  |\n| ++minus++ | Zoom out on chart (increase time range) |\n| ++equal++ | Reset zoom                              |\n\n## Mouse bindings\n\n| Binding      | Action                                                         |\n| ------------ | -------------------------------------------------------------- |\n| ++\"Scroll\"++ | Scrolling up or down zooms in or out of the graph respectively |\n"
  },
  {
    "path": "docs/content/usage/widgets/process.md",
    "content": "# Process Widget\n\nThe process widget displays a table containing information regarding a running process, along with sorting,\nsearching, and process control features.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/process_default.webp\" alt=\"A picture of an expanded process widget by default.\"/>\n</figure>\n\n## Features\n\nThe process widget has three main components:\n\n- The main process table\n- The search sub-widget (opened with ++ctrl+f++ or ++slash++)\n- The sort menu sub-widget (opened with ++s++ or ++f6++)\n\nBy default, the main process table displays the following information for each process:\n\n- PID\n- Name of the process\n- CPU use percentage (note this is averaged out per available thread by default)\n- Memory use percentage\n- Disk reads per second\n- Disk writes per second\n- Total amount read from disk\n- Total amount written from disk\n- User\n- Process state\n- Process uptime\n\n  <!-- 2-space indent here because mdx_truly_sane_lists interferes, see https://github.com/squidfunk/mkdocs-material/discussions/3763#discussioncomment-2833731 -->\n  !!! info indent\n\n      On Windows, the I/O counters will report _all_ reads/writes, not just disk. See\n      [here](https://docs.rs/sysinfo/latest/sysinfo/struct.Process.html#method.disk_usage)\n      for more details.\n\nWith the feature flag (`--disable_gpu` on Linux/Windows to disable) and gpu process columns enabled in the configuration:\n\n- GPU memory use percentage\n- GPU core utilization percentage\n\nSee [the processes configuration page](../../configuration/config-file/processes.md) on how to customize which columns\nare shown.\n\n### Sorting\n\nThe table can be sorted by clicking on the table headers, which will either sort the table by that column, or if already\nsorting by that column, reverse the sorting order.\n\nAlternatively, one can sort using the sort menu sub-widget, which is brought up using ++s++ or ++f6++, and can be controlled by arrow keys or the mouse.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/process_sort_menu.webp\" alt=\"A picture of an expanded process widget with the sort menu open.\"/>\n</figure>\n\n### Grouping\n\nPressing ++tab++ in the table will group entries with the same name together. The PID column will be replaced with the number of entries in each group, and usage\nis added together when displayed.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/process_grouped.webp\" alt=\"A picture of grouped mode in a process widget.\"/>\n</figure>\n\n!!! info\n\n    Note that the process state and user columns are disabled in this mode.\n\n!!! info\n\n    Note that if tree mode is also active, processes cannot be grouped together due to the behaviour of the two modes\n    somewhat clashing. This also reflects with default modes like `group_processes`.\n\n### Process termination\n\nPressing ++d+d++ or ++f9++ will allow you to terminate the currently selected process/process group. On Unix-like\noperating systems, you are also able to control which specific signals to send (e.g. `SIGKILL`, `SIGTERM`).\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/process_kill_linux.webp\" alt=\"A picture of the process kill menu on Linux.\"/>\n    <figcaption><sub>The process termination menu on Linux</sub></figcaption>\n</figure>\n\nIf you're on Windows, or if the `disable_advanced_kill` flag is set in the options or command-line (only available on\nLinux, macOS, and FreeBSD), then a simpler termination screen with just yes or no options will be shown.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/process_kill_simple.webp\" alt=\"A picture of the process kill menu on Windows.\"/>\n    <figcaption><sub>The process termination menu on Windows</sub></figcaption>\n</figure>\n\n### Tree mode\n\nPressing ++t++ or ++f5++ in the table toggles tree mode in the process widget, displaying processes in regard to their parent-child process relationships.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/process_tree.webp\" alt=\"A picture of tree mode in a process widget.\"/>\n</figure>\n\nA process in tree mode can also be \"collapsed\", hiding its children and any descendants, using the either the ++minus++,\n++plus++, or ++left++ keys, or clicking on an entry. It can be expanded by using the ++minus++, ++plus++, or ++right++\nkeys, or by clicking on the entry again. The ++space++ key can also be used to toggle between the collapsed and expanded states.\n\n!!! info\n\n    Note that if tree mode is active, processes cannot be grouped together due to the behaviour of the two modes\n    somewhat clashing. This also reflects with default modes like `group_processes`.\n\n### Full command\n\nYou can show the full command instead of just the process name by pressing ++P++.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/process_full.webp\" alt=\"A picture of a process widget using full commands.\"/>\n</figure>\n\n### Search\n\nPressing ++slash++ or ++ctrl+f++ will open up the search sub-widget. By default, just typing in something will search by the process name.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/search/search.webp\" alt=\"A picture of searching for a process with a simple search.\"/>\n</figure>\n\nThis search can be further enhanced by matching by case, matching the entire word, or by regex.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/search/regex.webp\" alt=\"A picture of searching for a process with a search condition that uses regex.\"/>\n</figure>\n\nWe are able to also search for multiple things/conditions.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/search/or.webp\" alt=\"A picture of searching for a process with a search condition that uses the or operator.\"/>\n</figure>\n\nAnd if our search uses a keyword, we need to use quotation marks around the term to properly search it.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/search/quotes.webp\" alt=\"A picture of searching for a process with a search condition that needs quotation marks.\"/>\n</figure>\n\nLastly, we can refine our search even further based on the other columns, like PID, CPU usage, etc., as well as grouping together conditions.\n\n<figure>\n    <img src=\"../../../assets/screenshots/process/search/cpu.webp\" alt=\"A picture of searching for a process with a search condition that uses the CPU keyword.\"/>\n</figure>\n\nYou can also paste search queries (e.g. ++shift+insert++, ++ctrl+shift+v++).\n\n#### Keywords\n\nNote all keywords are case-insensitive. To search for a process/command that collides with a keyword, surround the term with quotes (e.x. `\"cpu\"`).\n\n| Keywords                        | Example                               | Description                                                                      |\n| ------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------- |\n|                                 | `btm`                                 | Matches by process or command name; supports regex                               |\n| `pid`                           | `pid=1044`                            | Matches by PID; supports regex                                                   |\n| `cpu` <br/> `cpu%`              | `cpu > 0.5`                           | Matches the CPU column; supports comparison operators                            |\n| `memb`                          | `memb > 1000 b`                       | Matches the memory column in terms of bytes; supports comparison operators       |\n| `mem` <br/> `mem%`              | `mem < 0.5`                           | Matches the memory column in terms of percent; supports comparison operators     |\n| `read` <br/> `r/s` <br/> `rps`  | `read = 1 mb`                         | Matches the read/s column in terms of bytes; supports comparison operators       |\n| `write` <br/> `w/s` <br/> `wps` | `write >= 1 kb`                       | Matches the write/s column in terms of bytes; supports comparison operators      |\n| `tread` <br/> `t.read`          | `tread <= 1024 gb`                    | Matches the total read column in terms of bytes; supports comparison operators    |\n| `twrite` <br/> `t.write`        | `twrite > 1024 tb`                    | Matches the total write column in terms of bytes; supports comparison operators  |\n| `user`                          | `user=root`                           | Matches by user; supports regex                                                  |\n| `state`                         | `state=running`                       | Matches by state; supports regex                                                 |\n| `()`                            | `(<COND 1> AND <COND 2>) OR <COND 3>` | Group together a condition                                                       |\n| `gmem`                          | `gmem > 1000 b`                       | Matches the gpu memory column in terms of bytes; supports comparison operators   |\n| `gmem%`                         | `gmem% < 0.5`                         | Matches the gpu memory column in terms of percent; supports comparison operators |\n| `gpu%`                          | `gpu% > 0`                            | Matches the gpu usage column in terms of percent; supports comparison operators  |\n\n#### Comparison operators\n\n| Keywords | Description                                                    |\n| -------- | -------------------------------------------------------------- |\n| `=`      | Checks if the values are equal                                 |\n| `>`      | Checks if the left value is strictly greater than the right    |\n| `<`      | Checks if the left value is strictly less than the right       |\n| `>=`     | Checks if the left value is greater than or equal to the right |\n| `<=`     | Checks if the left value is less than or equal to the right    |\n\n#### Logical operators\n\nNote all operators are case-insensitive, and the `and` operator takes precedence over the `or` operator.\n\n| Keywords                             | Usage                                                                          | Description                                         |\n| ------------------------------------ | ------------------------------------------------------------------------------ | --------------------------------------------------- |\n| `and` <br/> `&&` <br/> `<Space>`     | `<COND 1> and <COND 2>` <br/> `<COND 1> && <COND 2>` <br/> `<COND 1> <COND 2>` | Requires both conditions to be true to match        |\n| `or` <br/> <code>&#124;&#124;</code> | `<COND 1> or <COND 2>` <br/> `<COND 1> &#124;&#124; <COND 2>`                  | Requires at least one condition to be true to match |\n\n#### Units\n\nAll units are case-insensitive.\n\n| Keywords | Description |\n| -------- | ----------- |\n| `B`      | Bytes       |\n| `KB`     | Kilobytes   |\n| `MB`     | Megabytes   |\n| `GB`     | Gigabytes   |\n| `TB`     | Terabytes   |\n| `KiB`    | Kibibytes   |\n| `MiB`    | Mebibytes   |\n| `GiB`    | Gibibytes   |\n| `TiB`    | Tebibytes   |\n\n## Key bindings\n\nNote that key bindings are generally case-sensitive.\n\n### Process table\n\n| Binding                                             | Action                                                           |\n| --------------------------------------------------- | ---------------------------------------------------------------- |\n| ++up++ , ++k++                                      | Move up within a widget                                          |\n| ++down++ , ++j++                                    | Move down within a widget                                        |\n| ++g+g++ , ++home++                                  | Jump to the first entry in the table                             |\n| ++G++ , ++end++                                     | Jump to the last entry in the table                              |\n| ++d+d++ , ++f9++                                    | Send a kill signal to the selected process                       |\n| ++c++                                               | Sort by CPU usage, press again to reverse sorting order          |\n| ++m++                                               | Sort by memory usage, press again to reverse sorting order       |\n| ++p++                                               | Sort by PID name, press again to reverse sorting order           |\n| ++n++                                               | Sort by process name, press again to reverse sorting order       |\n| ++tab++                                             | Toggle grouping processes with the same name                     |\n| ++P++                                               | Toggle between showing the full command or just the process name |\n| ++ctrl+f++ , ++slash++                              | Toggle showing the search sub-widget                             |\n| ++s++ , ++f6++, ++delete++ (++fn+delete++ on macOS) | Toggle showing the sort sub-widget                               |\n| ++I++                                               | Invert the current sort                                          |\n| ++\"%\"++                                             | Toggle between values and percentages for memory usage           |\n| ++t++ , ++f5++                                      | Toggle tree mode                                                 |\n| ++M++                                               | Sort by gpu memory usage, press again to reverse sorting order   |\n| ++C++                                               | Sort by gpu usage, press again to reverse sorting order          |\n| ++z++                                               | Toggle the hiding of kernel threads                              |\n\n### Sort sub-widget\n\n| Binding            | Action                                |\n| ------------------ | ------------------------------------- |\n| ++up++ , ++k++     | Move up within a widget               |\n| ++down++ , ++j++   | Move down within a widget             |\n| ++g+g++ , ++home++ | Jump to the first entry in the table  |\n| ++G++ , ++end++    | Jump to the last entry in the table   |\n| ++esc++            | Close the sort sub-widget             |\n| ++enter++          | Sorts the corresponding process table |\n\n### Search sub-widget\n\n| Binding                               | Action                                       |\n| ------------------------------------- | -------------------------------------------- |\n| ++left++ <br/> ++h++ <br/> ++alt+h++  | Moves the cursor left                        |\n| ++right++ <br/> ++l++ <br/> ++alt+l++ | Moves the cursor right                       |\n| ++esc++                               | Close the search widget (retains the filter) |\n| ++ctrl+a++                            | Skip to the start of the search query        |\n| ++ctrl+e++                            | Skip to the end of the search query          |\n| ++ctrl+u++                            | Clear the current search query               |\n| ++ctrl+w++                            | Delete a word behind the cursor              |\n| ++ctrl+h++                            | Delete the character behind the cursor       |\n| ++backspace++                         | Delete the character behind the cursor       |\n| ++delete++ (++fn+delete++ on macOS)   | Delete the character at the cursor           |\n| ++alt+c++ , ++f1++                    | Toggle matching case                         |\n| ++alt+w++ , ++f2++                    | Toggle matching the entire word              |\n| ++alt+r++ , ++f3++                    | Toggle using regex                           |\n\n## Mouse bindings\n\n### Process table\n\n| Binding      | Action                                                                                                                                                              |\n| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| ++\"Scroll\"++ | Selects a CPU thread/average to show in the graph                                                                                                                   |\n| ++lbutton++  | Table header: Sorts/reverse sorts the table by the column <br/> Table entry: Selects an entry in the table, if in tree mode, collapses/expands the entry's children |\n\n### Sort sub-widget\n\n| Binding     | Action                        |\n| ----------- | ----------------------------- |\n| ++lbutton++ | Selects an entry in the table |\n"
  },
  {
    "path": "docs/content/usage/widgets/temperature.md",
    "content": "# Temperature Widget\n\nThe temperature widget provides a table of temperature sensors and their current temperature.\n\n<figure>\n    <img src=\"../../../assets/screenshots/temperature.webp\" alt=\"A picture of an expanded temperature widget.\"/>\n</figure>\n\n## Features\n\nThe temperature widget provides the sensor name as well as its current temperature.\n\nThis widget can also be configured to display Nvidia and AMD GPU temperatures (`--disable_gpu` on Linux/Windows to disable).\n\n## Key bindings\n\nNote that key bindings are generally case-sensitive.\n\n| Binding            | Action                                                    |\n| ------------------ | --------------------------------------------------------- |\n| ++up++ , ++k++     | Move up within a widget                                   |\n| ++down++ , ++j++   | Move down within a widget                                 |\n| ++g+g++ , ++home++ | Jump to the first entry in the table                      |\n| ++G++ , ++end++    | Jump to the last entry in the table                       |\n| ++t++              | Sort by temperature, press again to reverse sorting order |\n| ++s++              | Sort by sensor name, press again to reverse sorting order |\n\n## Mouse bindings\n\n| Binding     | Action                        |\n| ----------- | ----------------------------- |\n| ++lbutton++ | Selects an entry in the table |\n"
  },
  {
    "path": "docs/hooks/nightly_banner.py",
    "content": "import os\nimport sys\n\nimport mkdocs.plugins\n\n\n@mkdocs.plugins.event_priority(-100)\ndef on_config(config):\n    print(\"Running nightly banner hook...\", file=sys.stderr)\n\n    # From https://github.com/jimporter/mike/blob/3351d5feabff8ee107f4ad6d1f86055843c7dbf1/mike/mkdocs_utils.py#L13\n    version = os.environ.get(\"MIKE_DOCS_VERSION\")\n    print(f\"Version: {version}\", file=sys.stderr)\n\n    if version == \"nightly\":\n        extra = config.get(\"extra\", {})\n        extra[\"nightly\"] = True\n"
  },
  {
    "path": "docs/hooks/nightly_redirect.py",
    "content": "import os\nimport sys\nimport json\nimport mkdocs.plugins\nimport urllib.request\n\n\n# Based on https://github.com/squidfunk/mkdocs-material/discussions/3758#discussioncomment-4397373\n\n\n@mkdocs.plugins.event_priority(-50)\ndef on_config(config):\n    print(\"Running nightly release redirect hook...\", file=sys.stderr)\n    try:\n        nightly_tag_name = None\n        override = os.environ.get(\"MKDOCS_NIGHTLY_RELEASE_OVERRIDE\")\n\n        if override:\n            nightly_tag_name = override\n        else:\n            with urllib.request.urlopen(\n                \"https://api.github.com/repos/ClementTsang/bottom/releases\"\n            ) as response:\n                raw_data = response.read()\n                data = json.loads(raw_data.decode(\"utf-8\"))\n\n                first_nightly = next(\n                    release for release in data if \"nightly-\" in release[\"tag_name\"]\n                )\n                nightly_tag_name = first_nightly[\"tag_name\"]\n\n        if nightly_tag_name is not None:\n            nightly_release_url = f\"https://github.com/ClementTsang/bottom/releases/tag/{nightly_tag_name}\"\n\n            redirect_plugin = config.get(\"plugins\", {}).get(\"redirects\")\n            redirects = redirect_plugin.config.get(\"redirect_maps\", {})\n            redirects[\"nightly-release.md\"] = nightly_release_url\n\n            print(\n                f\"Updated nightly release redirect to point to {nightly_release_url}\",\n                file=sys.stderr,\n            )\n        else:\n            print(\"nightly tag name was not set by any means.\")\n    except Exception as e:\n        print(\n            f\"error adjusting redirect, falling back to general releases page: {e}\",\n            file=sys.stderr,\n        )\n"
  },
  {
    "path": "docs/mike.sh",
    "content": "#!/bin/bash\n\n# Used to serve a versioned version of the docs locally. Note this\n# does NOT reflect local changes.\n\nset -e\n\nVENV_PATH=\"./.venv/\"\nPYTHON_CMD=${1:-python}\n\nif [ ! -d $VENV_PATH ]; then\n    echo \"venv not found, creating one using the command '${PYTHON_CMD}'...\";\n    $PYTHON_CMD -m venv .venv;\n    source $VENV_PATH/bin/activate;\n    pip install --upgrade pip;\n    pip install -r requirements.txt;\n    $VENV_PATH/bin/mike serve;\nelse\n    echo \"venv already found.\";\n    source $VENV_PATH/bin/activate;\n    pip install --upgrade pip;\n    pip install -r requirements.txt;\n    $VENV_PATH/bin/mike serve;\nfi;\n\n"
  },
  {
    "path": "docs/mkdocs.yml",
    "content": "# Site information\nsite_name: bottom\nsite_author: Clement Tsang\nsite_url: https://bottom.pages.dev\nsite_description: >-\n  A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows.\ndocs_dir: \"content/\"\n# Project information\nrepo_name: ClementTsang/bottom\nrepo_url: https://github.com/ClementTsang/bottom\nedit_uri: \"edit/main/docs/content/\"\ncopyright: Copyright &copy; 2019 - 2026 Clement Tsang\n\n# Theming\ntheme:\n  name: material\n  font:\n    code: IBM Plex Mono\n  features:\n    - content.action.edit\n    - navigation.expand\n    - navigation.footer\n    - navigation.indexes\n    - navigation.instant\n    - navigation.instant.progress\n    - navigation.sections\n    - navigation.tabs\n    - navigation.top\n    - search.highlight\n    - search.suggest\n    - toc.integrate\n    - toc.follow\n  icon:\n    edit: material/pencil\n  palette:\n    # Palette toggle for automatic mode\n    - media: \"(prefers-color-scheme)\"\n      scheme: default\n      toggle:\n        icon: material/brightness-auto\n        name: Switch to light mode\n    # Light mode\n    - media: \"(prefers-color-scheme: light)\"\n      primary: indigo\n      accent: indigo\n      toggle:\n        icon: material/weather-sunny\n        name: Switch to dark mode\n    # Dark mode\n    - media: \"(prefers-color-scheme: dark)\"\n      scheme: slate\n      primary: black\n      accent: indigo\n      toggle:\n        icon: material/weather-night\n        name: Switch to system preference\n  custom_dir: \"overrides\"\nextra_css:\n  - stylesheets/extra.css\n\n# Extensions\nmarkdown_extensions:\n  - admonition\n  - attr_list\n  - toc:\n      anchorlink: true\n  - pymdownx.inlinehilite\n  - pymdownx.keys:\n      # Override to make it case-sensitive\n      key_map:\n        {\n          \"a\": \"a\",\n          \"b\": \"b\",\n          \"c\": \"c\",\n          \"d\": \"d\",\n          \"e\": \"e\",\n          \"f\": \"f\",\n          \"g\": \"g\",\n          \"h\": \"h\",\n          \"i\": \"i\",\n          \"j\": \"j\",\n          \"k\": \"k\",\n          \"l\": \"l\",\n          \"m\": \"m\",\n          \"n\": \"n\",\n          \"o\": \"o\",\n          \"p\": \"p\",\n          \"q\": \"q\",\n          \"r\": \"r\",\n          \"s\": \"s\",\n          \"t\": \"t\",\n          \"u\": \"u\",\n          \"v\": \"v\",\n          \"w\": \"w\",\n          \"x\": \"x\",\n          \"y\": \"y\",\n          \"z\": \"z\",\n          \"A\": \"A\",\n          \"B\": \"B\",\n          \"C\": \"C\",\n          \"D\": \"D\",\n          \"E\": \"E\",\n          \"F\": \"F\",\n          \"G\": \"G\",\n          \"H\": \"H\",\n          \"I\": \"I\",\n          \"J\": \"J\",\n          \"K\": \"K\",\n          \"L\": \"L\",\n          \"M\": \"M\",\n          \"N\": \"N\",\n          \"O\": \"O\",\n          \"P\": \"P\",\n          \"Q\": \"Q\",\n          \"R\": \"R\",\n          \"S\": \"S\",\n          \"T\": \"T\",\n          \"U\": \"U\",\n          \"V\": \"V\",\n          \"W\": \"W\",\n          \"X\": \"X\",\n          \"Y\": \"Y\",\n          \"Z\": \"Z\",\n        }\n  - pymdownx.details\n  - pymdownx.highlight\n  - pymdownx.superfences\n  - mdx_truly_sane_lists # See https://github.com/mkdocs/mkdocs/issues/545#issuecomment-522196661\n  - pymdownx.tabbed:\n      alternate_style: true\n\nplugins:\n  - tags\n  - search\n  - mike:\n      canonical_version: stable\n  - git-revision-date-localized:\n      type: date\n  - privacy\n  - redirects:\n      redirect_maps:\n        nightly-release.md: \"https://github.com/ClementTsang/bottom/releases\"\n\nextra:\n  # Versioning\n  version:\n    provider: mike\n    default: stable\n    alias: true\n  # Used for the nightly banner\n  nightly: false\n\n# Navigation\nnav:\n  - \"Home\": index.md\n  - \"Support\":\n      - \"Official Support\": support/official.md\n      - \"Unofficial Support\": support/unofficial.md\n  - \"Usage\":\n      - \"General Usage\": usage/general-usage.md\n      - \"Basic Mode\": usage/basic-mode.md\n      - \"Widgets\":\n          - \"CPU Widget\": usage/widgets/cpu.md\n          - \"Memory Widget\": usage/widgets/memory.md\n          - \"Network Widget\": usage/widgets/network.md\n          - \"Process Widget\": usage/widgets/process.md\n          - \"Disk Widget\": usage/widgets/disk.md\n          - \"Temperature Widget\": usage/widgets/temperature.md\n          - \"Battery Widget\": usage/widgets/battery.md\n      - \"Auto-Complete\": usage/autocomplete.md\n  - \"Configuration\":\n      - \"Command-line Options\": configuration/command-line-options.md\n      - \"Config File\":\n          - configuration/config-file/index.md\n          - \"CPU Widget\": configuration/config-file/cpu.md\n          - \"Disk Table Widget\": configuration/config-file/disk-table.md\n          - \"Network Widget\": configuration/config-file/network.md\n          - \"Processes Widget\": configuration/config-file/processes.md\n          - \"Temperature Table Widget\": configuration/config-file/temperature-table.md\n          - \"Flags\": configuration/config-file/flags.md\n          - \"Layout\": configuration/config-file/layout.md\n          - \"Styling\": configuration/config-file/styling.md\n  - \"Contribution\":\n      - \"Issues, Pull Requests, and Discussions\": contribution/issues-and-pull-requests.md\n      - \"Documentation\": contribution/documentation.md\n      - \"Packaging and Distribution\": contribution/packaging-and-distribution.md\n      - \"Development\":\n          - \"Development Environment\": contribution/development/dev_env.md\n          - \"Testing\": contribution/development/testing.md\n          - \"Logging\": contribution/development/logging.md\n          - \"Build Process\": contribution/development/build_process.md\n          - \"Deploy Process\": contribution/development/deploy_process.md\n  - \"Troubleshooting\": troubleshooting.md\n\nhooks:\n  - ./hooks/nightly_redirect.py\n  - ./hooks/nightly_banner.py\n\nexclude_docs: |\n  nightly-release.md\n"
  },
  {
    "path": "docs/overrides/main.html",
    "content": "{% extends \"base.html\" %}\n{% block announce %}\n    {#- It's like helm and you need to add dashes, see https://github.com/squidfunk/mkdocs-material/discussions/5803#discussioncomment-7690065; note that _Jinja_ comments need to be used, not HTML -#}\n    {%- if config.extra.nightly -%}\n    {#- Need to reapply margin from base CSS, which is overridden in extra CSS (to fix empty banner) -#}\n    <div style=\"margin: 0.6rem auto\">\n        This is <strong>nightly</strong> documentation, and it may differ from stable. Please see <strong><a href=\"{{ '../' ~ base_url }}\">here for stable documentation</a></strong>.\n    </div>\n    {%- endif -%}\n{% endblock %}\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "mkdocs == 1.6.1\nmkdocs-material == 9.7.1\nmdx_truly_sane_lists == 1.3\nmike == 2.1.3\nmkdocs-git-revision-date-localized-plugin == 1.4.5\nmkdocs-redirects == 1.2.2\n"
  },
  {
    "path": "docs/serve.sh",
    "content": "#!/bin/bash\n\nset -e\n\nVENV_PATH=\"./.venv/\"\nPYTHON_CMD=${1:-python}\n\nif [ ! -d $VENV_PATH ]; then\n    echo \"venv not found, creating one using the command '${PYTHON_CMD}'...\";\n    $PYTHON_CMD -m venv .venv;\n    source $VENV_PATH/bin/activate;\n    pip install --upgrade pip;\n    pip install -r requirements.txt;\n    $VENV_PATH/bin/mkdocs serve;\nelse\n    echo \"venv already found.\";\n    source $VENV_PATH/bin/activate;\n    pip install --upgrade pip;\n    pip install -r requirements.txt;\n    $VENV_PATH/bin/mkdocs serve;\nfi;\n\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "reorder_imports = true\nreorder_modules = true\nmerge_derives = true\nfn_params_layout = \"Compressed\"\nuse_field_init_shorthand = true\ntab_spaces = 4\nmax_width = 100\nstyle_edition = \"2024\"\n\n# Unstable options, disabled by default.\n\n# I usually re-enable these periodically and run cargo +nightly fmt.\n# imports_granularity = \"Crate\"\n# group_imports = \"StdExternalCrate\"\n\n# These sometimes break things, be careful re-enabling them.\n# wrap_comments = true\n# format_code_in_doc_comments = true\n"
  },
  {
    "path": "sample_configs/default_config.toml",
    "content": "# This is a default config file for bottom. All of the settings are commented\n# out by default; if you wish to change them uncomment and modify as you see\n# fit.\n\n# This group of options represents a command-line option. Flags explicitly\n# added when running (ie: btm -a) will override this config file if an option\n# is also set here.\n[flags]\n# Whether to hide the average cpu entry.\n#hide_avg_cpu = false\n\n# Whether to use a dedicated row for the average cpu entry\n#average_cpu_row = false\n\n# Whether to use dot markers rather than braille.\n#dot_marker = false\n\n# The update rate of the application.\n#rate = \"1s\"\n\n# Whether to put the CPU legend to the left.\n#cpu_left_legend = false\n\n# Whether to set CPU% on a process to be based on the total CPU or just current usage.\n#current_usage = false\n\n# Whether to set CPU% on a process to be based on the total CPU or per-core CPU% (not divided by the number of cpus).\n#unnormalized_cpu = false\n\n# Whether to group processes with the same name together by default. Doesn't do anything\n# if tree is set to true or --tree is set.\n#group_processes = false\n\n# Whether to make process searching case sensitive by default.\n#case_sensitive = false\n\n# Whether to make process searching look for matching the entire word by default.\n#whole_word = false\n\n# Whether to make process searching use regex by default.\n#regex = false\n\n# The temperature unit. One of the following, defaults to \"c\" for Celsius:\n#temperature_type = \"c\"\n##temperature_type = \"k\"\n##temperature_type = \"f\"\n##temperature_type = \"kelvin\"\n##temperature_type = \"fahrenheit\"\n##temperature_type = \"celsius\"\n\n# The default time interval (in milliseconds).\n#default_time_value = \"60s\"\n\n# The time delta on each zoom in/out action (in milliseconds).\n#time_delta = 15000\n\n# Hides the time scale.\n#hide_time = false\n\n# Override layout default widget\n#default_widget_type = \"proc\"\n#default_widget_count = 1\n\n# Expand selected widget upon starting the app\n#expanded = true\n\n# Use basic mode\n#basic = false\n\n# Use the old network legend style\n#use_old_network_legend = false\n\n# Remove space in tables\n#hide_table_gap = false\n\n# Show the battery widgets\n#battery = false\n\n# Disable mouse clicks\n#disable_click = false\n\n# Disable keyboard shortcuts\n#disable_keys = false\n\n# Show memory values in the processes widget as values by default\n#process_memory_as_value = false\n\n# Show tree mode by default in the processes widget.\n#tree = false\n\n# Shows an indicator in table widgets tracking where in the list you are.\n#show_table_scroll_position = false\n\n# Show processes as their commands by default in the process widget.\n#process_command = false\n\n# Displays the network widget with binary prefixes.\n#network_use_binary_prefix = false\n\n# Displays the network widget using bytes.\n#network_use_bytes = false\n\n# Displays the network widget with a log scale.\n#network_use_log = false\n\n# Hides advanced options to stop a process on Unix-like systems.\n#disable_advanced_kill = false\n\n# Prevents performing any actions that affect the system (e.g. stopping processes).\n#read_only = false\n\n# Hides the kernel threads\n#hide_k_threads = false\n\n# Hide GPU(s) information\n#disable_gpu = false\n\n# Shows cache and buffer memory\n#enable_cache_memory = false\n\n# Subtract freeable ARC from memory usage\n#free_arc = false\n\n# How much data is stored at once in terms of time.\n#retention = \"10m\"\n\n# Where to place the legend for the memory widget. One of \"none\", \"top-left\", \"top\", \"top-right\", \"left\", \"right\", \"bottom-left\", \"bottom\", \"bottom-right\".\n#memory_legend = \"top-right\"\n\n# Where to place the legend for the network widget. One of \"none\", \"top-left\", \"top\", \"top-right\", \"left\", \"right\", \"bottom-left\", \"bottom\", \"bottom-right\".\n#network_legend = \"top-right\"\n\n\n# Processes widget configuration\n#[processes]\n# The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built):\n# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%, Nice, Priority\n#columns = [\"PID\", \"Name\", \"CPU%\", \"Mem%\", \"Virt\", \"R/s\", \"W/s\", \"T.Read\", \"T.Write\", \"User\", \"State\", \"GMem%\", \"GPU%\", \"Priority\", \"Nice\"]\n\n# Gather process child thread information\n#get_threads = false\n\n\n# CPU widget configuration\n#[cpu]\n# One of \"all\" (default), \"average\"/\"avg\"\n#default = \"average\"\n\n\n# Disk widget configuration\n#[disk]\n# The columns shown by the process widget. The following columns are supported:\n# Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s\n#columns = [\"Disk\", \"Mount\", \"Used\", \"Free\", \"Total\", \"Used%\", \"R/s\", \"W/s\"]\n\n# By default, there are no disk name filters enabled. These can be turned on to filter out specific data entries if you\n# don't want to see them. An example use case is provided below.\n#[disk.name_filter]\n# Whether to ignore any matches. Defaults to true.\n#is_list_ignored = true\n\n# A list of filters to try and match.\n#list = [\"/dev/sda\\\\d+\", \"/dev/nvme0n1p2\"]\n\n# Whether to use regex. Defaults to false.\n#regex = true\n\n# Whether to be case-sensitive. Defaults to false.\n#case_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\n#whole_word = false\n\n# By default, there are no mount name filters enabled. An example use case is provided below.\n#[disk.mount_filter]\n# Whether to ignore any matches. Defaults to true.\n#is_list_ignored = true\n\n# A list of filters to try and match.\n#list = [\"/mnt/.*\", \"/boot\"]\n\n# Whether to use regex. Defaults to false.\n#regex = true\n\n# Whether to be case-sensitive. Defaults to false.\n#case_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\n#whole_word = false\n\n\n# Temperature widget configuration\n#[temperature]\n# By default, there are no temperature sensor filters enabled. An example use case is provided below.\n#[temperature.sensor_filter]\n# Whether to ignore any matches. Defaults to true.\n#is_list_ignored = true\n\n# A list of filters to try and match.\n#list = [\"cpu\", \"wifi\"]\n\n# Whether to use regex. Defaults to false.\n#regex = false\n\n# Whether to be case-sensitive. Defaults to false.\n#case_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\n#whole_word = false\n\n\n# Network widget configuration\n#[network]\n# By default, there are no network interface filters enabled. An example use case is provided below.\n#[network.interface_filter]\n# Whether to ignore any matches. Defaults to true.\n#is_list_ignored = true\n\n# A list of filters to try and match.\n#list = [\"virbr0.*\"]\n\n# Whether to use regex. Defaults to false.\n#regex = true\n\n# Whether to be case-sensitive. Defaults to false.\n#case_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\n#whole_word = false\n\n\n# These are all the components that support custom theming.  Note that colour support\n# will depend on terminal support.\n#[styles] # Uncomment if you want to use custom styling\n# Built-in themes. Valid values are:\n# - \"default\"\n# - \"default-light\"\n# - \"gruvbox\"\n# - \"gruvbox-light\"\n# - \"nord\"\n# - \"nord-light\".\n#\n# This will have the lowest precedence if a custom colour palette is set,\n# or overridden if the command-line flag for a built-in theme is set.\n#theme = \"default\"\n\n#[styles.cpu]\n#all_entry_color = \"green\"\n#avg_entry_color = \"red\"\n#cpu_core_colors = [\"light magenta\", \"light yellow\", \"light cyan\", \"light green\", \"light blue\", \"cyan\", \"green\", \"blue\"]\n\n#[styles.memory]\n#ram_color = \"light magenta\"\n#cache_color = \"light red\"\n#swap_color = \"light yellow\"\n#arc_color = \"light cyan\"\n#gpu_colors = [\"light blue\", \"light red\", \"cyan\", \"green\", \"blue\", \"red\"]\n\n#[styles.network]\n#rx_color = \"light magenta\"\n#tx_color = \"light yellow\"\n#rx_total_color = \"light cyan\"\n#tx_total_color = \"light green\"\n\n#[styles.battery]\n#high_battery_color = \"green\"\n#medium_battery_color = \"yellow\"\n#low_battery_color = \"red\"\n\n#[styles.tables]\n#headers = {color = \"light blue\", bold = true}\n\n#[styles.graphs]\n#graph_color = \"gray\"\n#legend_text = {color = \"gray\"}\n\n#[styles.widgets]\n#border_color = \"gray\"\n#selected_border_color = \"light blue\"\n#widget_title = {color = \"gray\"}\n#text = {color = \"gray\"}\n#selected_text = {color = \"black\", bg_color = \"light blue\"}\n#disabled_text = {color = \"dark gray\"}\n\n# Only on Linux\n#thread_text = {color = \"green\"}\n\n# Layout - layouts follow a pattern like this:\n# [[row]] represents a row in the application.\n# [[row.child]] represents either a widget or a column.\n# [[row.child.child]] represents a widget.\n#\n# All widgets must have the type value set to one of [\"cpu\", \"mem\", \"proc\", \"net\", \"temp\", \"disk\", \"empty\"].\n# All layout components have a ratio value - if this is not set, then it defaults to 1.\n# The default widget layout:\n#[[row]]\n#  ratio=30\n#  [[row.child]]\n#  type=\"cpu\"\n#[[row]]\n#    ratio=40\n#    [[row.child]]\n#      ratio=4\n#      type=\"mem\"\n#    [[row.child]]\n#      ratio=3\n#      [[row.child.child]]\n#        type=\"temp\"\n#      [[row.child.child]]\n#        type=\"disk\"\n#[[row]]\n#  ratio=30\n#  [[row.child]]\n#    type=\"net\"\n#  [[row.child]]\n#    type=\"proc\"\n#    default=true\n"
  },
  {
    "path": "sample_configs/demo_config.toml",
    "content": "[flags]\navg_cpu = true\n\n# Temperature is one of:\ntemperature_type = \"c\"\n\nrate = 1000\ncpu_left_legend = false\ncurrent_usage = false\ngroup_processes = false\ncase_sensitive = false\nwhole_word = false\nregex = true\ndefault_widget_type = \"cpu\"\ndefault_widget_count = 1\n\n[styles]\ntheme = \"gruvbox\"\n\n[processes]\ncolumns = [\"PID\", \"Name\", \"CPU%\", \"Mem%\", \"Rps\", \"Wps\", \"TRead\", \"TWrite\", \"State\", \"Time\", \"Virt\"]\n"
  },
  {
    "path": "schema/README.md",
    "content": "# Config JSON Schema\n\n## Generation\n\nThese are automatically generated from code using [`schemars`](https://github.com/GREsau/schemars). They're locked\nbehind a feature flag to avoid building unnecessary code for release builds, and you can generate them like so:\n\n```bash\n# Will print out to stdout\ncargo run --features=\"generate_schema\" -- --generate_schema\n\n# e.g. for nightly\ncargo run --features=\"generate_schema\" -- --generate_schema > schema/nightly/bottom.json\n\n# e.g. for a specific version\ncargo run --features=\"generate_schema\" -- --generate_schema 0.12.0 > schema/v0.12.0/bottom.json\n```\n\nAlternatively, run the `scripts/schema/generate.sh` script (for stable releases) or `scripts/schema/nightly.sh`\n(for nightly), which does all of this for you.\n\n## Publication\n\nTo publish these schemas:\n\n### Stable\n\n1. Run `scripts/schema/generate.sh <YOUR_VERSION>`.\n2. Make a PR and merge it.\n3. Then, make a PR to [schemastore](https://github.com/SchemaStore/schemastore) to update the catalog.\n   [Here's an example of a PR](https://github.com/SchemaStore/schemastore/pull/5242).\n\n### Nightly\n\n1. Run `scripts/schema/nightly.sh`.\n2. Make a PR and merge it.\n"
  },
  {
    "path": "schema/nightly/bottom.json",
    "content": "{\n  \"$id\": \"https://github.com/ClementTsang/bottom/blob/main/schema/nightly/bottom.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Schema for bottom's config file (nightly)\",\n  \"description\": \"https://bottom.pages.dev/nightly/configuration/config-file/\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"cpu\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/CpuConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"disk\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/DiskConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"flags\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/GeneralConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"network\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/NetworkConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"processes\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/ProcessesConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"row\": {\n      \"type\": [\n        \"array\",\n        \"null\"\n      ],\n      \"items\": {\n        \"$ref\": \"#/$defs/row\"\n      }\n    },\n    \"styles\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/StyleConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"temperature\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/TempConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    }\n  },\n  \"$defs\": {\n    \"BatteryStyle\": {\n      \"description\": \"Styling specific to the battery widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"high_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery is over 50%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"low_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery is under 10%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"medium_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery between 10% to 50%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"ColorStr\": {\n      \"type\": \"string\"\n    },\n    \"CpuConfig\": {\n      \"description\": \"CPU column settings.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"default\": {\n          \"description\": \"The default selected entry of the CPU widget.\",\n          \"$ref\": \"#/$defs/CpuDefault\"\n        }\n      }\n    },\n    \"CpuDefault\": {\n      \"description\": \"The default selected entry of the CPU widget.\",\n      \"type\": \"string\",\n      \"enum\": [\n        \"all\",\n        \"average\"\n      ]\n    },\n    \"CpuStyle\": {\n      \"description\": \"Styling specific to the CPU widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"all_entry_color\": {\n          \"description\": \"The colour of the \\\"All\\\" CPU label.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"avg_entry_color\": {\n          \"description\": \"The colour of the average CPU label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cpu_core_colors\": {\n          \"description\": \"Colour of each CPU threads' label and graph line. Read in order.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/ColorStr\"\n          }\n        }\n      }\n    },\n    \"DiskColumn\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"Disk\",\n        \"Free\",\n        \"Free%\",\n        \"Mount\",\n        \"R/s\",\n        \"Read\",\n        \"Rps\",\n        \"Total\",\n        \"Used\",\n        \"Used%\",\n        \"W/s\",\n        \"Wps\",\n        \"Write\"\n      ]\n    },\n    \"DiskConfig\": {\n      \"description\": \"Disk configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"columns\": {\n          \"description\": \"A list of disk widget columns.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/DiskColumn\"\n          }\n        },\n        \"mount_filter\": {\n          \"description\": \"A filter over the mount names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"name_filter\": {\n          \"description\": \"A filter over the disk names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"FinalWidget\": {\n      \"description\": \"Represents a widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"default\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"ratio\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint16\",\n          \"maximum\": 65535,\n          \"minimum\": 0\n        },\n        \"type\": {\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ]\n    },\n    \"GeneralConfig\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"autohide_time\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"average_cpu_row\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"basic\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"battery\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"case_sensitive\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"cpu_left_legend\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"current_usage\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"default_time_value\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"default_widget_count\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint64\",\n          \"minimum\": 0\n        },\n        \"default_widget_type\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"disable_advanced_kill\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"disable_click\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"disable_gpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"disable_keys\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"dot_marker\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"enable_cache_memory\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"expanded\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"free_arc\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"group_processes\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_avg_cpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_k_threads\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_table_gap\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_time\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"memory_legend\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"network_legend\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"network_use_binary_prefix\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"network_use_bytes\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"network_use_log\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"no_write\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"process_command\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"process_memory_as_value\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"rate\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"read_only\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"regex\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"retention\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"show_table_scroll_position\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"temperature_type\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"time_delta\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tree\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"tree_collapse\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"unnormalized_cpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"use_old_network_legend\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"whole_word\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        }\n      }\n    },\n    \"GraphStyle\": {\n      \"description\": \"General styling for graph widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"graph_color\": {\n          \"description\": \"The general colour of the parts of the graph.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"legend_text\": {\n          \"description\": \"Text styling for graph's legend text.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"IgnoreList\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"case_sensitive\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"is_list_ignored\": {\n          \"type\": \"boolean\",\n          \"default\": true\n        },\n        \"list\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"regex\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"whole_word\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        }\n      },\n      \"required\": [\n        \"list\"\n      ]\n    },\n    \"MemoryStyle\": {\n      \"description\": \"Styling specific to the memory widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"arc_color\": {\n          \"description\": \"The colour of the ARC label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cache_color\": {\n          \"description\": \"The colour of the cache label and graph line. Does not do anything on Windows.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"gpu_colors\": {\n          \"description\": \"Colour of each GPU's memory label and graph line. Read in order.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/ColorStr\"\n          }\n        },\n        \"ram_color\": {\n          \"description\": \"The colour of the RAM label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"swap_color\": {\n          \"description\": \"The colour of the swap label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"NetworkConfig\": {\n      \"description\": \"Network configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"interface_filter\": {\n          \"description\": \"A filter over the network interface names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"NetworkStyle\": {\n      \"description\": \"Styling specific to the network widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"rx_color\": {\n          \"description\": \"The colour of the RX (download) label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"rx_total_color\": {\n          \"description\": \"he colour of the total RX (download) label in basic mode.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tx_color\": {\n          \"description\": \"The colour of the TX (upload) label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tx_total_color\": {\n          \"description\": \"The colour of the total TX (upload) label in basic mode.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"ProcColumn\": {\n      \"description\": \"A column in the process widget.\",\n      \"type\": \"string\",\n      \"enum\": [\n        \"CPU%\",\n        \"Command\",\n        \"Count\",\n        \"GMem\",\n        \"GMem%\",\n        \"GPU%\",\n        \"Mem\",\n        \"Mem%\",\n        \"Memory\",\n        \"Memory%\",\n        \"Name\",\n        \"Nice\",\n        \"PID\",\n        \"Priority\",\n        \"R/s\",\n        \"Read\",\n        \"Rps\",\n        \"State\",\n        \"T.Read\",\n        \"T.Write\",\n        \"TRead\",\n        \"TWrite\",\n        \"Time\",\n        \"Total Read\",\n        \"Total Write\",\n        \"User\",\n        \"Virt\",\n        \"VirtMem\",\n        \"Virtual\",\n        \"Virtual Memory\",\n        \"W/s\",\n        \"Wps\",\n        \"Write\"\n      ]\n    },\n    \"ProcessesConfig\": {\n      \"description\": \"Process configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"columns\": {\n          \"description\": \"A list of process widget columns.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/$defs/ProcColumn\"\n          }\n        },\n        \"get_threads\": {\n          \"description\": \"Whether to get process child threads.\",\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        }\n      }\n    },\n    \"RowChildren\": {\n      \"description\": \"Represents a child of a Row - either a Col (column) or a FinalWidget.\\n\\nA Col can also have an optional length and children.  We only allow columns\\nto have FinalWidgets as children, lest we get some amount of mutual\\nrecursion between Row and Col.\",\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/FinalWidget\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"child\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/$defs/FinalWidget\"\n              }\n            },\n            \"ratio\": {\n              \"type\": [\n                \"integer\",\n                \"null\"\n              ],\n              \"format\": \"uint16\",\n              \"maximum\": 65535,\n              \"minimum\": 0\n            }\n          },\n          \"required\": [\n            \"child\"\n          ]\n        }\n      ]\n    },\n    \"StringOrNum\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"integer\",\n          \"format\": \"uint64\",\n          \"minimum\": 0\n        }\n      ]\n    },\n    \"StyleConfig\": {\n      \"description\": \"Style-related configs.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"battery\": {\n          \"description\": \"Styling for the battery widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/BatteryStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cpu\": {\n          \"description\": \"Styling for the CPU widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/CpuStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"graphs\": {\n          \"description\": \"Styling for graph widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/GraphStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"memory\": {\n          \"description\": \"Styling for the memory widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/MemoryStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"network\": {\n          \"description\": \"Styling for the network widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/NetworkStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tables\": {\n          \"description\": \"Styling for table widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TableStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"theme\": {\n          \"description\": \"A built-in theme.\\n\\nIf this is and a custom colour are both set, in the config file,\\nthe custom colour scheme will be prioritized first. If a theme\\nis set in the command-line args, however, it will always be\\nprioritized first.\",\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"widgets\": {\n          \"description\": \"Styling for general widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/WidgetStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TableStyle\": {\n      \"description\": \"General styling for table widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"headers\": {\n          \"description\": \"Text styling for table headers.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TempConfig\": {\n      \"description\": \"Temperature configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"sensor_filter\": {\n          \"description\": \"A filter over the sensor names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TextStyleConfig\": {\n      \"description\": \"A style for text.\",\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/ColorStr\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"bg_color\": {\n              \"description\": \"A built-in ANSI colour, RGB hex, or RGB colour code.\",\n              \"anyOf\": [\n                {\n                  \"$ref\": \"#/$defs/ColorStr\"\n                },\n                {\n                  \"type\": \"null\"\n                }\n              ]\n            },\n            \"bold\": {\n              \"description\": \"Whether to make this text bolded or not. If not set,\\nwill default to built-in defaults.\",\n              \"type\": [\n                \"boolean\",\n                \"null\"\n              ]\n            },\n            \"color\": {\n              \"description\": \"A built-in ANSI colour, RGB hex, or RGB colour code.\",\n              \"anyOf\": [\n                {\n                  \"$ref\": \"#/$defs/ColorStr\"\n                },\n                {\n                  \"type\": \"null\"\n                }\n              ]\n            },\n            \"italics\": {\n              \"description\": \"Whether to make this text italicized or not. If not set,\\nwill default to built-in defaults.\",\n              \"type\": [\n                \"boolean\",\n                \"null\"\n              ]\n            }\n          }\n        }\n      ]\n    },\n    \"WidgetBorderType\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"Default\",\n        \"Rounded\",\n        \"Double\",\n        \"Thick\"\n      ]\n    },\n    \"WidgetStyle\": {\n      \"description\": \"General styling for generic widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"border_color\": {\n          \"description\": \"The colour of the widgets' borders.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"disabled_text\": {\n          \"description\": \"Text styling for text when representing something that is disabled.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"selected_border_color\": {\n          \"description\": \"The colour of a widget's borders when the widget is selected.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"selected_text\": {\n          \"description\": \"Text styling for text when representing something that is selected.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"text\": {\n          \"description\": \"Text styling for text in general.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"thread_text\": {\n          \"description\": \"Text styling for text when representing process threads. Only usable\\non Linux at the moment.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"widget_border_type\": {\n          \"description\": \"Widget borders type.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/WidgetBorderType\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"widget_title\": {\n          \"description\": \"Text styling for a widget's title.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"row\": {\n      \"description\": \"Represents a row. This has a length of some sort (optional) and a vector\\nof children.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"child\": {\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/RowChildren\"\n          }\n        },\n        \"ratio\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint16\",\n          \"maximum\": 65535,\n          \"minimum\": 0\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "schema/v0.10/bottom.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://github.com/ClementTsang/bottom/blob/main/schema/v0.10/bottom.json\",\n  \"title\": \"Schema for bottom's configs (v0.10)\",\n  \"description\": \"https://clementtsang.github.io/bottom/0.10.0/configuration/config-file/\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"cpu\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/definitions/CpuConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"disk\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/definitions/DiskConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"flags\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/definitions/FlagConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"network\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/definitions/NetworkConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"processes\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/definitions/ProcessesConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"row\": {\n      \"type\": [\n        \"array\",\n        \"null\"\n      ],\n      \"items\": {\n        \"$ref\": \"#/definitions/row\"\n      }\n    },\n    \"styles\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/definitions/StyleConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"temperature\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/definitions/TempConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    }\n  },\n  \"definitions\": {\n    \"BatteryStyle\": {\n      \"description\": \"Styling specific to the battery widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"high_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery is over 50%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"low_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery is under 10%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"medium_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery between 10% to 50%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"ColorStr\": {\n      \"type\": \"string\"\n    },\n    \"CpuConfig\": {\n      \"description\": \"CPU column settings.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"default\": {\n          \"$ref\": \"#/definitions/CpuDefault\"\n        }\n      }\n    },\n    \"CpuDefault\": {\n      \"description\": \"The default selection of the CPU widget. If the given selection is invalid, we will fall back to all.\",\n      \"type\": \"string\",\n      \"enum\": [\n        \"all\",\n        \"average\"\n      ]\n    },\n    \"CpuStyle\": {\n      \"description\": \"Styling specific to the CPU widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"all_entry_color\": {\n          \"description\": \"The colour of the \\\"All\\\" CPU label.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"avg_entry_color\": {\n          \"description\": \"The colour of the average CPU label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cpu_core_colors\": {\n          \"description\": \"Colour of each CPU threads' label and graph line. Read in order.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/definitions/ColorStr\"\n          }\n        }\n      }\n    },\n    \"DiskConfig\": {\n      \"description\": \"Disk configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"mount_filter\": {\n          \"description\": \"A filter over the mount names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"name_filter\": {\n          \"description\": \"A filter over the disk names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"FinalWidget\": {\n      \"description\": \"Represents a widget.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"type\"\n      ],\n      \"properties\": {\n        \"default\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"ratio\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint32\",\n          \"minimum\": 0.0\n        },\n        \"type\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"FlagConfig\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"autohide_time\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"basic\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"battery\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"case_sensitive\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"cpu_left_legend\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"current_usage\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"default_time_value\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"default_widget_count\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint64\",\n          \"minimum\": 0.0\n        },\n        \"default_widget_type\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"disable_advanced_kill\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"disable_click\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"dot_marker\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"enable_cache_memory\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"enable_gpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"expanded\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"group_processes\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_avg_cpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_table_gap\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_time\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"memory_legend\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"network_legend\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"network_use_binary_prefix\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"network_use_bytes\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"network_use_log\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"no_write\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"process_command\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"process_memory_as_value\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"rate\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"regex\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"retention\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"show_table_scroll_position\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"temperature_type\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"time_delta\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tree\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"unnormalized_cpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"use_old_network_legend\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"whole_word\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        }\n      }\n    },\n    \"GraphStyle\": {\n      \"description\": \"General styling for graph widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"graph_color\": {\n          \"description\": \"The general colour of the parts of the graph.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"legend_text\": {\n          \"description\": \"Text styling for graph's legend text.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"IgnoreList\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"list\"\n      ],\n      \"properties\": {\n        \"case_sensitive\": {\n          \"default\": false,\n          \"type\": \"boolean\"\n        },\n        \"is_list_ignored\": {\n          \"default\": true,\n          \"type\": \"boolean\"\n        },\n        \"list\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"regex\": {\n          \"default\": false,\n          \"type\": \"boolean\"\n        },\n        \"whole_word\": {\n          \"default\": false,\n          \"type\": \"boolean\"\n        }\n      }\n    },\n    \"MemoryStyle\": {\n      \"description\": \"Styling specific to the memory widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"arc_color\": {\n          \"description\": \"The colour of the ARC label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cache_color\": {\n          \"description\": \"The colour of the cache label and graph line. Does not do anything on Windows.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"gpu_colors\": {\n          \"description\": \"Colour of each GPU's memory label and graph line. Read in order.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/definitions/ColorStr\"\n          }\n        },\n        \"ram_color\": {\n          \"description\": \"The colour of the RAM label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"swap_color\": {\n          \"description\": \"The colour of the swap label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"NetworkConfig\": {\n      \"description\": \"Network configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"interface_filter\": {\n          \"description\": \"A filter over the network interface names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"NetworkStyle\": {\n      \"description\": \"Styling specific to the network widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"rx_color\": {\n          \"description\": \"The colour of the RX (download) label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"rx_total_color\": {\n          \"description\": \"he colour of the total RX (download) label in basic mode.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tx_color\": {\n          \"description\": \"The colour of the TX (upload) label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tx_total_color\": {\n          \"description\": \"The colour of the total TX (upload) label in basic mode.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"ProcColumn\": {\n      \"description\": \"A column in the process widget.\",\n      \"type\": \"string\",\n      \"enum\": [\n        \"PID\",\n        \"Count\",\n        \"Name\",\n        \"Command\",\n        \"CPU%\",\n        \"Mem\",\n        \"Mem%\",\n        \"R/s\",\n        \"Read\",\n        \"Rps\",\n        \"W/s\",\n        \"Write\",\n        \"Wps\",\n        \"T.Read\",\n        \"TWrite\",\n        \"T.Write\",\n        \"TRead\",\n        \"State\",\n        \"User\",\n        \"Time\",\n        \"GMem\",\n        \"GMem%\",\n        \"GPU%\"\n      ]\n    },\n    \"ProcessesConfig\": {\n      \"description\": \"Process configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"columns\": {\n          \"description\": \"A list of process widget columns.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/ProcColumn\"\n          }\n        }\n      }\n    },\n    \"RowChildren\": {\n      \"description\": \"Represents a child of a Row - either a Col (column) or a FinalWidget.\\n\\nA Col can also have an optional length and children.  We only allow columns to have FinalWidgets as children, lest we get some amount of mutual recursion between Row and Col.\",\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/definitions/FinalWidget\"\n        },\n        {\n          \"type\": \"object\",\n          \"required\": [\n            \"child\"\n          ],\n          \"properties\": {\n            \"child\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/definitions/FinalWidget\"\n              }\n            },\n            \"ratio\": {\n              \"type\": [\n                \"integer\",\n                \"null\"\n              ],\n              \"format\": \"uint32\",\n              \"minimum\": 0.0\n            }\n          }\n        }\n      ]\n    },\n    \"StringOrNum\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"integer\",\n          \"format\": \"uint64\",\n          \"minimum\": 0.0\n        }\n      ]\n    },\n    \"StyleConfig\": {\n      \"description\": \"Style-related configs.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"battery\": {\n          \"description\": \"Styling for the battery widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/BatteryStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cpu\": {\n          \"description\": \"Styling for the CPU widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/CpuStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"graphs\": {\n          \"description\": \"Styling for graph widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/GraphStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"memory\": {\n          \"description\": \"Styling for the memory widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/MemoryStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"network\": {\n          \"description\": \"Styling for the network widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/NetworkStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tables\": {\n          \"description\": \"Styling for table widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/TableStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"theme\": {\n          \"description\": \"A built-in theme.\\n\\nIf this is and a custom colour are both set, in the config file, the custom colour scheme will be prioritized first. If a theme is set in the command-line args, however, it will always be prioritized first.\",\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"widgets\": {\n          \"description\": \"Styling for general widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/WidgetStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TableStyle\": {\n      \"description\": \"General styling for table widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"headers\": {\n          \"description\": \"Text styling for table headers.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TempConfig\": {\n      \"description\": \"Temperature configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"sensor_filter\": {\n          \"description\": \"A filter over the sensor names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TextStyleConfig\": {\n      \"description\": \"A style for text.\",\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/definitions/ColorStr\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"bg_color\": {\n              \"description\": \"A built-in ANSI colour, RGB hex, or RGB colour code.\",\n              \"anyOf\": [\n                {\n                  \"$ref\": \"#/definitions/ColorStr\"\n                },\n                {\n                  \"type\": \"null\"\n                }\n              ]\n            },\n            \"bold\": {\n              \"description\": \"Whether to make this text bolded or not. If not set, will default to built-in defaults.\",\n              \"type\": [\n                \"boolean\",\n                \"null\"\n              ]\n            },\n            \"color\": {\n              \"description\": \"A built-in ANSI colour, RGB hex, or RGB colour code.\",\n              \"anyOf\": [\n                {\n                  \"$ref\": \"#/definitions/ColorStr\"\n                },\n                {\n                  \"type\": \"null\"\n                }\n              ]\n            },\n            \"italics\": {\n              \"description\": \"Whether to make this text italicized or not. If not set, will default to built-in defaults.\",\n              \"type\": [\n                \"boolean\",\n                \"null\"\n              ]\n            }\n          }\n        }\n      ]\n    },\n    \"WidgetStyle\": {\n      \"description\": \"General styling for generic widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"border_color\": {\n          \"description\": \"The colour of the widgets' borders.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"disabled_text\": {\n          \"description\": \"Text styling for text when representing something that is disabled.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"selected_border_color\": {\n          \"description\": \"The colour of a widget's borders when the widget is selected.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"selected_text\": {\n          \"description\": \"Text styling for text when representing something that is selected.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"text\": {\n          \"description\": \"Text styling for text in general.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"widget_title\": {\n          \"description\": \"Text styling for a widget's title.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"row\": {\n      \"description\": \"Represents a row. This has a length of some sort (optional) and a vector of children.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"child\": {\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/definitions/RowChildren\"\n          }\n        },\n        \"ratio\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint32\",\n          \"minimum\": 0.0\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "schema/v0.11/bottom.json",
    "content": "{\n  \"$id\": \"https://github.com/ClementTsang/bottom/blob/main/schema/nightly/bottom.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Schema for bottom's config file (v0.11)\",\n  \"description\": \"https://bottom.pages.dev/0.11.0/configuration/config-file/\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"cpu\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/CpuConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"disk\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/DiskConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"flags\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/GeneralConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"network\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/NetworkConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"processes\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/ProcessesConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"row\": {\n      \"type\": [\n        \"array\",\n        \"null\"\n      ],\n      \"items\": {\n        \"$ref\": \"#/$defs/row\"\n      }\n    },\n    \"styles\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/StyleConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"temperature\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/TempConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    }\n  },\n  \"$defs\": {\n    \"BatteryStyle\": {\n      \"description\": \"Styling specific to the battery widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"high_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery is over 50%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"low_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery is under 10%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"medium_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery between 10% to 50%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"ColorStr\": {\n      \"type\": \"string\"\n    },\n    \"CpuConfig\": {\n      \"description\": \"CPU column settings.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"default\": {\n          \"$ref\": \"#/$defs/CpuDefault\"\n        }\n      }\n    },\n    \"CpuDefault\": {\n      \"description\": \"The default selection of the CPU widget. If the given selection is invalid,\\nwe will fall back to all.\",\n      \"type\": \"string\",\n      \"enum\": [\n        \"all\",\n        \"average\"\n      ]\n    },\n    \"CpuStyle\": {\n      \"description\": \"Styling specific to the CPU widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"all_entry_color\": {\n          \"description\": \"The colour of the \\\"All\\\" CPU label.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"avg_entry_color\": {\n          \"description\": \"The colour of the average CPU label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cpu_core_colors\": {\n          \"description\": \"Colour of each CPU threads' label and graph line. Read in order.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/ColorStr\"\n          }\n        }\n      }\n    },\n    \"DiskColumn\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"Disk\",\n        \"Free\",\n        \"Free%\",\n        \"Mount\",\n        \"R/s\",\n        \"Read\",\n        \"Rps\",\n        \"Total\",\n        \"Used\",\n        \"Used%\",\n        \"W/s\",\n        \"Wps\",\n        \"Write\"\n      ]\n    },\n    \"DiskConfig\": {\n      \"description\": \"Disk configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"columns\": {\n          \"description\": \"A list of disk widget columns.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/DiskColumn\"\n          }\n        },\n        \"mount_filter\": {\n          \"description\": \"A filter over the mount names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"name_filter\": {\n          \"description\": \"A filter over the disk names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"FinalWidget\": {\n      \"description\": \"Represents a widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"default\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"ratio\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint16\",\n          \"maximum\": 65535,\n          \"minimum\": 0\n        },\n        \"type\": {\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ]\n    },\n    \"GeneralConfig\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"autohide_time\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"average_cpu_row\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"basic\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"battery\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"case_sensitive\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"cpu_left_legend\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"current_usage\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"default_time_value\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"default_widget_count\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint64\",\n          \"minimum\": 0\n        },\n        \"default_widget_type\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"disable_advanced_kill\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"disable_click\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"disable_gpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"disable_keys\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"dot_marker\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"enable_cache_memory\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"expanded\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"free_arc\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"group_processes\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_avg_cpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_k_threads\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_table_gap\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_time\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"memory_legend\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"network_legend\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"network_use_binary_prefix\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"network_use_bytes\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"network_use_log\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"no_write\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"process_command\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"process_memory_as_value\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"rate\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"regex\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"retention\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"show_table_scroll_position\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"temperature_type\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"time_delta\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tree\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"tree_collapse\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"unnormalized_cpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"use_old_network_legend\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"whole_word\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        }\n      }\n    },\n    \"GraphStyle\": {\n      \"description\": \"General styling for graph widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"graph_color\": {\n          \"description\": \"The general colour of the parts of the graph.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"legend_text\": {\n          \"description\": \"Text styling for graph's legend text.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"IgnoreList\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"case_sensitive\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"is_list_ignored\": {\n          \"type\": \"boolean\",\n          \"default\": true\n        },\n        \"list\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"regex\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"whole_word\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        }\n      },\n      \"required\": [\n        \"list\"\n      ]\n    },\n    \"MemoryStyle\": {\n      \"description\": \"Styling specific to the memory widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"arc_color\": {\n          \"description\": \"The colour of the ARC label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cache_color\": {\n          \"description\": \"The colour of the cache label and graph line. Does not do anything on Windows.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"gpu_colors\": {\n          \"description\": \"Colour of each GPU's memory label and graph line. Read in order.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/ColorStr\"\n          }\n        },\n        \"ram_color\": {\n          \"description\": \"The colour of the RAM label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"swap_color\": {\n          \"description\": \"The colour of the swap label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"NetworkConfig\": {\n      \"description\": \"Network configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"interface_filter\": {\n          \"description\": \"A filter over the network interface names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"NetworkStyle\": {\n      \"description\": \"Styling specific to the network widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"rx_color\": {\n          \"description\": \"The colour of the RX (download) label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"rx_total_color\": {\n          \"description\": \"he colour of the total RX (download) label in basic mode.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tx_color\": {\n          \"description\": \"The colour of the TX (upload) label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tx_total_color\": {\n          \"description\": \"The colour of the total TX (upload) label in basic mode.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"ProcColumn\": {\n      \"description\": \"A column in the process widget.\",\n      \"type\": \"string\",\n      \"enum\": [\n        \"CPU%\",\n        \"Command\",\n        \"Count\",\n        \"GMem\",\n        \"GMem%\",\n        \"GPU%\",\n        \"Mem\",\n        \"Mem%\",\n        \"Memory\",\n        \"Memory%\",\n        \"Name\",\n        \"PID\",\n        \"R/s\",\n        \"Read\",\n        \"Rps\",\n        \"State\",\n        \"T.Read\",\n        \"T.Write\",\n        \"TRead\",\n        \"TWrite\",\n        \"Time\",\n        \"Total Read\",\n        \"Total Write\",\n        \"User\",\n        \"Virt\",\n        \"VirtMem\",\n        \"Virtual\",\n        \"Virtual Memory\",\n        \"W/s\",\n        \"Wps\",\n        \"Write\"\n      ]\n    },\n    \"ProcessesConfig\": {\n      \"description\": \"Process configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"columns\": {\n          \"description\": \"A list of process widget columns.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/$defs/ProcColumn\"\n          }\n        },\n        \"get_threads\": {\n          \"description\": \"Whether to get process child threads.\",\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        }\n      }\n    },\n    \"RowChildren\": {\n      \"description\": \"Represents a child of a Row - either a Col (column) or a FinalWidget.\\n\\nA Col can also have an optional length and children.  We only allow columns\\nto have FinalWidgets as children, lest we get some amount of mutual\\nrecursion between Row and Col.\",\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/FinalWidget\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"child\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/$defs/FinalWidget\"\n              }\n            },\n            \"ratio\": {\n              \"type\": [\n                \"integer\",\n                \"null\"\n              ],\n              \"format\": \"uint16\",\n              \"maximum\": 65535,\n              \"minimum\": 0\n            }\n          },\n          \"required\": [\n            \"child\"\n          ]\n        }\n      ]\n    },\n    \"StringOrNum\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"integer\",\n          \"format\": \"uint64\",\n          \"minimum\": 0\n        }\n      ]\n    },\n    \"StyleConfig\": {\n      \"description\": \"Style-related configs.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"battery\": {\n          \"description\": \"Styling for the battery widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/BatteryStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cpu\": {\n          \"description\": \"Styling for the CPU widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/CpuStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"graphs\": {\n          \"description\": \"Styling for graph widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/GraphStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"memory\": {\n          \"description\": \"Styling for the memory widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/MemoryStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"network\": {\n          \"description\": \"Styling for the network widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/NetworkStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tables\": {\n          \"description\": \"Styling for table widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TableStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"theme\": {\n          \"description\": \"A built-in theme.\\n\\nIf this is and a custom colour are both set, in the config file,\\nthe custom colour scheme will be prioritized first. If a theme\\nis set in the command-line args, however, it will always be\\nprioritized first.\",\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"widgets\": {\n          \"description\": \"Styling for general widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/WidgetStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TableStyle\": {\n      \"description\": \"General styling for table widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"headers\": {\n          \"description\": \"Text styling for table headers.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TempConfig\": {\n      \"description\": \"Temperature configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"sensor_filter\": {\n          \"description\": \"A filter over the sensor names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TextStyleConfig\": {\n      \"description\": \"A style for text.\",\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/ColorStr\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"bg_color\": {\n              \"description\": \"A built-in ANSI colour, RGB hex, or RGB colour code.\",\n              \"anyOf\": [\n                {\n                  \"$ref\": \"#/$defs/ColorStr\"\n                },\n                {\n                  \"type\": \"null\"\n                }\n              ]\n            },\n            \"bold\": {\n              \"description\": \"Whether to make this text bolded or not. If not set,\\nwill default to built-in defaults.\",\n              \"type\": [\n                \"boolean\",\n                \"null\"\n              ]\n            },\n            \"color\": {\n              \"description\": \"A built-in ANSI colour, RGB hex, or RGB colour code.\",\n              \"anyOf\": [\n                {\n                  \"$ref\": \"#/$defs/ColorStr\"\n                },\n                {\n                  \"type\": \"null\"\n                }\n              ]\n            },\n            \"italics\": {\n              \"description\": \"Whether to make this text italicized or not. If not set,\\nwill default to built-in defaults.\",\n              \"type\": [\n                \"boolean\",\n                \"null\"\n              ]\n            }\n          }\n        }\n      ]\n    },\n    \"WidgetBorderType\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"Default\",\n        \"Rounded\",\n        \"Double\",\n        \"Thick\"\n      ]\n    },\n    \"WidgetStyle\": {\n      \"description\": \"General styling for generic widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"border_color\": {\n          \"description\": \"The colour of the widgets' borders.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"disabled_text\": {\n          \"description\": \"Text styling for text when representing something that is disabled.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"selected_border_color\": {\n          \"description\": \"The colour of a widget's borders when the widget is selected.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"selected_text\": {\n          \"description\": \"Text styling for text when representing something that is selected.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"text\": {\n          \"description\": \"Text styling for text in general.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"thread_text\": {\n          \"description\": \"Text styling for text when representing process threads. Only usable\\non Linux at the moment.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"widget_border_type\": {\n          \"description\": \"Widget borders type.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/WidgetBorderType\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"widget_title\": {\n          \"description\": \"Text styling for a widget's title.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"row\": {\n      \"description\": \"Represents a row. This has a length of some sort (optional) and a vector\\nof children.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"child\": {\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/RowChildren\"\n          }\n        },\n        \"ratio\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint16\",\n          \"maximum\": 65535,\n          \"minimum\": 0\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "schema/v0.12.0/bottom.json",
    "content": "{\n  \"$id\": \"https://github.com/ClementTsang/bottom/blob/main/schema/0.12.0/bottom.json\",\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"title\": \"Schema for bottom's config file (v0.12.0)\",\n  \"description\": \"https://bottom.pages.dev/0.12.0/configuration/config-file/\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"cpu\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/CpuConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"disk\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/DiskConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"flags\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/GeneralConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"network\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/NetworkConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"processes\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/ProcessesConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"row\": {\n      \"type\": [\n        \"array\",\n        \"null\"\n      ],\n      \"items\": {\n        \"$ref\": \"#/$defs/row\"\n      }\n    },\n    \"styles\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/StyleConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    },\n    \"temperature\": {\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/TempConfig\"\n        },\n        {\n          \"type\": \"null\"\n        }\n      ]\n    }\n  },\n  \"$defs\": {\n    \"BatteryStyle\": {\n      \"description\": \"Styling specific to the battery widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"high_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery is over 50%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"low_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery is under 10%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"medium_battery_color\": {\n          \"description\": \"The colour of the battery widget bar when the battery between 10% to 50%.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"ColorStr\": {\n      \"type\": \"string\"\n    },\n    \"CpuConfig\": {\n      \"description\": \"CPU column settings.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"default\": {\n          \"description\": \"The default selected entry of the CPU widget.\",\n          \"$ref\": \"#/$defs/CpuDefault\"\n        }\n      }\n    },\n    \"CpuDefault\": {\n      \"description\": \"The default selected entry of the CPU widget.\",\n      \"type\": \"string\",\n      \"enum\": [\n        \"all\",\n        \"average\"\n      ]\n    },\n    \"CpuStyle\": {\n      \"description\": \"Styling specific to the CPU widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"all_entry_color\": {\n          \"description\": \"The colour of the \\\"All\\\" CPU label.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"avg_entry_color\": {\n          \"description\": \"The colour of the average CPU label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cpu_core_colors\": {\n          \"description\": \"Colour of each CPU threads' label and graph line. Read in order.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/ColorStr\"\n          }\n        }\n      }\n    },\n    \"DiskColumn\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"Disk\",\n        \"Free\",\n        \"Free%\",\n        \"Mount\",\n        \"R/s\",\n        \"Read\",\n        \"Rps\",\n        \"Total\",\n        \"Used\",\n        \"Used%\",\n        \"W/s\",\n        \"Wps\",\n        \"Write\"\n      ]\n    },\n    \"DiskConfig\": {\n      \"description\": \"Disk configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"columns\": {\n          \"description\": \"A list of disk widget columns.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/DiskColumn\"\n          }\n        },\n        \"mount_filter\": {\n          \"description\": \"A filter over the mount names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"name_filter\": {\n          \"description\": \"A filter over the disk names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"FinalWidget\": {\n      \"description\": \"Represents a widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"default\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"ratio\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint16\",\n          \"maximum\": 65535,\n          \"minimum\": 0\n        },\n        \"type\": {\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\n        \"type\"\n      ]\n    },\n    \"GeneralConfig\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"autohide_time\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"average_cpu_row\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"basic\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"battery\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"case_sensitive\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"cpu_left_legend\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"current_usage\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"default_time_value\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"default_widget_count\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint64\",\n          \"minimum\": 0\n        },\n        \"default_widget_type\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"disable_advanced_kill\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"disable_click\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"disable_gpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"disable_keys\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"dot_marker\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"enable_cache_memory\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"expanded\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"free_arc\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"group_processes\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_avg_cpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_k_threads\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_table_gap\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"hide_time\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"memory_legend\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"network_legend\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"network_use_binary_prefix\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"network_use_bytes\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"network_use_log\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"no_write\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"process_command\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"process_memory_as_value\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"rate\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"read_only\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"regex\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"retention\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"show_table_scroll_position\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"temperature_type\": {\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"time_delta\": {\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/StringOrNum\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tree\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"tree_collapse\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"unnormalized_cpu\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"use_old_network_legend\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        },\n        \"whole_word\": {\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        }\n      }\n    },\n    \"GraphStyle\": {\n      \"description\": \"General styling for graph widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"graph_color\": {\n          \"description\": \"The general colour of the parts of the graph.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"legend_text\": {\n          \"description\": \"Text styling for graph's legend text.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"IgnoreList\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"case_sensitive\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"is_list_ignored\": {\n          \"type\": \"boolean\",\n          \"default\": true\n        },\n        \"list\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"regex\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"whole_word\": {\n          \"type\": \"boolean\",\n          \"default\": false\n        }\n      },\n      \"required\": [\n        \"list\"\n      ]\n    },\n    \"MemoryStyle\": {\n      \"description\": \"Styling specific to the memory widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"arc_color\": {\n          \"description\": \"The colour of the ARC label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cache_color\": {\n          \"description\": \"The colour of the cache label and graph line. Does not do anything on Windows.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"gpu_colors\": {\n          \"description\": \"Colour of each GPU's memory label and graph line. Read in order.\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/ColorStr\"\n          }\n        },\n        \"ram_color\": {\n          \"description\": \"The colour of the RAM label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"swap_color\": {\n          \"description\": \"The colour of the swap label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"NetworkConfig\": {\n      \"description\": \"Network configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"interface_filter\": {\n          \"description\": \"A filter over the network interface names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"NetworkStyle\": {\n      \"description\": \"Styling specific to the network widget.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"rx_color\": {\n          \"description\": \"The colour of the RX (download) label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"rx_total_color\": {\n          \"description\": \"he colour of the total RX (download) label in basic mode.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tx_color\": {\n          \"description\": \"The colour of the TX (upload) label and graph line.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tx_total_color\": {\n          \"description\": \"The colour of the total TX (upload) label in basic mode.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"ProcColumn\": {\n      \"description\": \"A column in the process widget.\",\n      \"type\": \"string\",\n      \"enum\": [\n        \"CPU%\",\n        \"Command\",\n        \"Count\",\n        \"GMem\",\n        \"GMem%\",\n        \"GPU%\",\n        \"Mem\",\n        \"Mem%\",\n        \"Memory\",\n        \"Memory%\",\n        \"Name\",\n        \"Nice\",\n        \"PID\",\n        \"Priority\",\n        \"R/s\",\n        \"Read\",\n        \"Rps\",\n        \"State\",\n        \"T.Read\",\n        \"T.Write\",\n        \"TRead\",\n        \"TWrite\",\n        \"Time\",\n        \"Total Read\",\n        \"Total Write\",\n        \"User\",\n        \"Virt\",\n        \"VirtMem\",\n        \"Virtual\",\n        \"Virtual Memory\",\n        \"W/s\",\n        \"Wps\",\n        \"Write\"\n      ]\n    },\n    \"ProcessesConfig\": {\n      \"description\": \"Process configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"columns\": {\n          \"description\": \"A list of process widget columns.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/$defs/ProcColumn\"\n          }\n        },\n        \"get_threads\": {\n          \"description\": \"Whether to get process child threads.\",\n          \"type\": [\n            \"boolean\",\n            \"null\"\n          ]\n        }\n      }\n    },\n    \"RowChildren\": {\n      \"description\": \"Represents a child of a Row - either a Col (column) or a FinalWidget.\\n\\nA Col can also have an optional length and children.  We only allow columns\\nto have FinalWidgets as children, lest we get some amount of mutual\\nrecursion between Row and Col.\",\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/FinalWidget\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"child\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"$ref\": \"#/$defs/FinalWidget\"\n              }\n            },\n            \"ratio\": {\n              \"type\": [\n                \"integer\",\n                \"null\"\n              ],\n              \"format\": \"uint16\",\n              \"maximum\": 65535,\n              \"minimum\": 0\n            }\n          },\n          \"required\": [\n            \"child\"\n          ]\n        }\n      ]\n    },\n    \"StringOrNum\": {\n      \"anyOf\": [\n        {\n          \"type\": \"string\"\n        },\n        {\n          \"type\": \"integer\",\n          \"format\": \"uint64\",\n          \"minimum\": 0\n        }\n      ]\n    },\n    \"StyleConfig\": {\n      \"description\": \"Style-related configs.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"battery\": {\n          \"description\": \"Styling for the battery widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/BatteryStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"cpu\": {\n          \"description\": \"Styling for the CPU widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/CpuStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"graphs\": {\n          \"description\": \"Styling for graph widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/GraphStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"memory\": {\n          \"description\": \"Styling for the memory widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/MemoryStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"network\": {\n          \"description\": \"Styling for the network widget.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/NetworkStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"tables\": {\n          \"description\": \"Styling for table widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TableStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"theme\": {\n          \"description\": \"A built-in theme.\\n\\nIf this is and a custom colour are both set, in the config file,\\nthe custom colour scheme will be prioritized first. If a theme\\nis set in the command-line args, however, it will always be\\nprioritized first.\",\n          \"type\": [\n            \"string\",\n            \"null\"\n          ]\n        },\n        \"widgets\": {\n          \"description\": \"Styling for general widgets.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/WidgetStyle\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TableStyle\": {\n      \"description\": \"General styling for table widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"headers\": {\n          \"description\": \"Text styling for table headers.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TempConfig\": {\n      \"description\": \"Temperature configuration.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"sensor_filter\": {\n          \"description\": \"A filter over the sensor names.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/IgnoreList\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"TextStyleConfig\": {\n      \"description\": \"A style for text.\",\n      \"anyOf\": [\n        {\n          \"$ref\": \"#/$defs/ColorStr\"\n        },\n        {\n          \"type\": \"object\",\n          \"properties\": {\n            \"bg_color\": {\n              \"description\": \"A built-in ANSI colour, RGB hex, or RGB colour code.\",\n              \"anyOf\": [\n                {\n                  \"$ref\": \"#/$defs/ColorStr\"\n                },\n                {\n                  \"type\": \"null\"\n                }\n              ]\n            },\n            \"bold\": {\n              \"description\": \"Whether to make this text bolded or not. If not set,\\nwill default to built-in defaults.\",\n              \"type\": [\n                \"boolean\",\n                \"null\"\n              ]\n            },\n            \"color\": {\n              \"description\": \"A built-in ANSI colour, RGB hex, or RGB colour code.\",\n              \"anyOf\": [\n                {\n                  \"$ref\": \"#/$defs/ColorStr\"\n                },\n                {\n                  \"type\": \"null\"\n                }\n              ]\n            },\n            \"italics\": {\n              \"description\": \"Whether to make this text italicized or not. If not set,\\nwill default to built-in defaults.\",\n              \"type\": [\n                \"boolean\",\n                \"null\"\n              ]\n            }\n          }\n        }\n      ]\n    },\n    \"WidgetBorderType\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"Default\",\n        \"Rounded\",\n        \"Double\",\n        \"Thick\"\n      ]\n    },\n    \"WidgetStyle\": {\n      \"description\": \"General styling for generic widgets.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"border_color\": {\n          \"description\": \"The colour of the widgets' borders.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"disabled_text\": {\n          \"description\": \"Text styling for text when representing something that is disabled.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"selected_border_color\": {\n          \"description\": \"The colour of a widget's borders when the widget is selected.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/ColorStr\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"selected_text\": {\n          \"description\": \"Text styling for text when representing something that is selected.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"text\": {\n          \"description\": \"Text styling for text in general.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"thread_text\": {\n          \"description\": \"Text styling for text when representing process threads. Only usable\\non Linux at the moment.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"widget_border_type\": {\n          \"description\": \"Widget borders type.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/WidgetBorderType\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"widget_title\": {\n          \"description\": \"Text styling for a widget's title.\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/$defs/TextStyleConfig\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        }\n      }\n    },\n    \"row\": {\n      \"description\": \"Represents a row. This has a length of some sort (optional) and a vector\\nof children.\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"child\": {\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/$defs/RowChildren\"\n          }\n        },\n        \"ratio\": {\n          \"type\": [\n            \"integer\",\n            \"null\"\n          ],\n          \"format\": \"uint16\",\n          \"maximum\": 65535,\n          \"minimum\": 0\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "schema/v0.9/bottom.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://github.com/ClementTsang/bottom/blob/main/schema/v0.9/bottom.json\",\n  \"$comment\": \"https://clementtsang.github.io/bottom/0.9.6/configuration/config-file/default-config/\",\n  \"title\": \"Schema for bottom's configs (v0.9)\",\n  \"type\": \"object\",\n  \"definitions\": {\n    \"row\": {\n      \"items\": {\n        \"properties\": {\n          \"ratio\": {\n            \"default\": 1,\n            \"type\": \"integer\"\n          },\n          \"type\": {\n            \"enum\": [\"cpu\", \"mem\", \"proc\", \"net\", \"temp\", \"disk\", \"empty\"],\n            \"type\": \"string\"\n          },\n          \"default\": {\n            \"default\": true,\n            \"type\": \"boolean\"\n          }\n        },\n        \"patternProperties\": {\n          \"row(.child)+\": {\n            \"$ref\": \"#/definitions/row\"\n          }\n        },\n        \"type\": \"object\"\n      },\n      \"type\": \"array\"\n    },\n    \"filter\": {\n      \"description\": \"hide specific temperature sensors, network interfaces, and disks using filters\",\n      \"properties\": {\n        \"is_list_ignored\": {\n          \"default\": true,\n          \"type\": \"boolean\"\n        },\n        \"list\": {\n          \"type\": \"array\"\n        },\n        \"regex\": {\n          \"default\": true,\n          \"type\": \"boolean\"\n        },\n        \"case_sensitive\": {\n          \"default\": false,\n          \"type\": \"boolean\"\n        },\n        \"whole_word\": {\n          \"default\": false,\n          \"type\": \"boolean\"\n        }\n      },\n      \"type\": \"object\"\n    }\n  },\n  \"properties\": {\n    \"flags\": {\n      \"description\": \"This group of options represents a command-line flag/option.  Flags explicitly added when running (ie: btm -a) will override this config file if an option is also set here\",\n      \"properties\": {\n        \"hide_avg_cpu\": {\n          \"default\": false,\n          \"description\": \"Whether to hide the average cpu entry\",\n          \"type\": \"boolean\"\n        },\n        \"dot_marker\": {\n          \"default\": false,\n          \"description\": \"Whether to use dot markers rather than braille\",\n          \"type\": \"boolean\"\n        },\n        \"rate\": {\n          \"default\": 1000,\n          \"description\": \"The update rate of the application\",\n          \"type\": \"integer\"\n        },\n        \"left_legend\": {\n          \"default\": false,\n          \"description\": \"Whether to put the CPU legend to the left\",\n          \"type\": \"boolean\"\n        },\n        \"current_usage\": {\n          \"default\": false,\n          \"description\": \"Whether to set CPU% on a process to be based on the total CPU or just current usage\",\n          \"type\": \"boolean\"\n        },\n        \"unnormalized_cpu\": {\n          \"default\": false,\n          \"description\": \"Whether to set CPU% on a process to be based on the total CPU or per-core CPU% (not divided by the number of cpus)\",\n          \"type\": \"boolean\"\n        },\n        \"group_processes\": {\n          \"default\": false,\n          \"description\": \"Whether to group processes with the same name together by default\",\n          \"type\": \"boolean\"\n        },\n        \"case_sensitive\": {\n          \"default\": false,\n          \"description\": \"Whether to make process searching case sensitive by default\",\n          \"type\": \"boolean\"\n        },\n        \"whole_word\": {\n          \"default\": false,\n          \"description\": \"Whether to make process searching look for matching the entire word by default\",\n          \"type\": \"boolean\"\n        },\n        \"regex\": {\n          \"default\": false,\n          \"description\": \"Whether to make process searching use regex by default\",\n          \"type\": \"boolean\"\n        },\n        \"temperature_type\": {\n          \"default\": \"k\",\n          \"enum\": [\"k\", \"f\", \"c\", \"kelvin\", \"fahrenheit\", \"celsius\"],\n          \"description\": \"Defaults to Celsius\",\n          \"type\": \"string\"\n        },\n        \"default_time_value\": {\n          \"default\": 60000,\n          \"description\": \"The default time interval in milliseconds\",\n          \"type\": \"integer\"\n        },\n        \"time_delta\": {\n          \"default\": 15000,\n          \"description\": \"The time delta on each zoom in/out action in milliseconds\",\n          \"type\": \"integer\"\n        },\n        \"hide_time\": {\n          \"default\": false,\n          \"description\": \"Hides the time scale\",\n          \"type\": \"boolean\"\n        },\n        \"default_widget_type\": {\n          \"default\": \"proc\",\n          \"description\": \"Override layout default widget\",\n          \"type\": \"string\"\n        },\n        \"default_widget_count\": {\n          \"default\": 1,\n          \"description\": \"Override layout default widget\",\n          \"type\": \"integer\"\n        },\n        \"expanded_on_startup\": {\n          \"default\": true,\n          \"description\": \"Expand selected widget upon starting the app\",\n          \"type\": \"boolean\"\n        },\n        \"basic\": {\n          \"default\": false,\n          \"description\": \"Use basic mode\",\n          \"type\": \"boolean\"\n        },\n        \"use_old_network_legend\": {\n          \"default\": false,\n          \"description\": \"Use the old network legend style\",\n          \"type\": \"boolean\"\n        },\n        \"hide_table_gap\": {\n          \"default\": false,\n          \"description\": \"Remove space in tables\",\n          \"type\": \"boolean\"\n        },\n        \"battery\": {\n          \"default\": false,\n          \"description\": \"Show the battery widgets\",\n          \"type\": \"boolean\"\n        },\n        \"disable_click\": {\n          \"default\": false,\n          \"description\": \"Disable mouse clicks\",\n          \"type\": \"boolean\"\n        },\n        \"color\": {\n          \"default\": \"default\",\n          \"enum\": [\n            \"default\",\n            \"default-light\",\n            \"gruvbox\",\n            \"gruvbox-light\",\n            \"nord\",\n            \"nord-light\"\n          ],\n          \"description\": \"Built-in themes\",\n          \"type\": \"string\"\n        },\n        \"process_memory_as_value\": {\n          \"default\": false,\n          \"description\": \"Show memory values in the processes widget as values by default\",\n          \"type\": \"boolean\"\n        },\n        \"tree\": {\n          \"default\": false,\n          \"description\": \"Show tree mode by default in the processes widget\",\n          \"type\": \"boolean\"\n        },\n        \"show_table_scroll_position\": {\n          \"default\": false,\n          \"description\": \"Shows an indicator in table widgets tracking where in the list you are\",\n          \"type\": \"boolean\"\n        },\n        \"process_command\": {\n          \"default\": false,\n          \"description\": \"Show processes as their commands by default in the process widget\",\n          \"type\": \"boolean\"\n        },\n        \"network_use_binary_prefix\": {\n          \"default\": false,\n          \"description\": \"Displays the network widget with binary prefixes\",\n          \"type\": \"boolean\"\n        },\n        \"network_use_bytes\": {\n          \"default\": false,\n          \"description\": \"Displays the network widget using bytes\",\n          \"type\": \"boolean\"\n        },\n        \"network_use_log\": {\n          \"default\": false,\n          \"description\": \"Displays the network widget with a log scale\",\n          \"type\": \"boolean\"\n        },\n        \"disable_advanced_kill\": {\n          \"default\": false,\n          \"description\": \"Hides advanced options to stop a process on Unix-like systems\",\n          \"type\": \"boolean\"\n        },\n        \"enable_gpu_memory\": {\n          \"default\": false,\n          \"description\": \"Shows GPU(s) memory\",\n          \"type\": \"boolean\"\n        },\n        \"retention\": {\n          \"default\": \"10m\",\n          \"description\": \"How much data is stored at once in terms of time\",\n          \"type\": \"string\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"colors\": {\n      \"description\": \"These are all the components that support custom theming.  Note that colour support will depend on terminal support\",\n      \"properties\": {\n        \"table_header_color\": {\n          \"default\": \"LightBlue\",\n          \"description\": \"Represents the colour of table headers (processes, CPU, disks, temperature)\",\n          \"type\": \"string\"\n        },\n        \"widget_title_color\": {\n          \"default\": \"Gray\",\n          \"description\": \"Represents the colour of the label each widget has\",\n          \"type\": \"string\"\n        },\n        \"avg_cpu_color\": {\n          \"default\": \"Red\",\n          \"description\": \"Represents the average CPU color\",\n          \"type\": \"string\"\n        },\n        \"cpu_core_colors\": {\n          \"items\": {\n            \"uniqueItems\": true,\n            \"minItems\": 1,\n            \"type\": \"string\"\n          },\n          \"default\": [\n            \"LightMagenta\",\n            \"LightYellow\",\n            \"LightCyan\",\n            \"LightGreen\",\n            \"LightBlue\",\n            \"LightRed\",\n            \"Cyan\",\n            \"Green\",\n            \"Blue\",\n            \"Red\"\n          ],\n          \"description\": \"Represents the colour the core will use in the CPU legend and graph\",\n          \"type\": \"array\"\n        },\n        \"ram_color\": {\n          \"default\": \"LightMagenta\",\n          \"description\": \"Represents the colour RAM will use in the memory legend and graph\",\n          \"type\": \"string\"\n        },\n        \"swap_color\": {\n          \"default\": \"LightYellow\",\n          \"description\": \"Represents the colour SWAP will use in the memory legend and graph\",\n          \"type\": \"string\"\n        },\n        \"arc_color\": {\n          \"default\": \"LightCyan\",\n          \"description\": \"Represents the colour ARC will use in the memory legend and graph\",\n          \"type\": \"string\"\n        },\n        \"gpu_core_colors\": {\n          \"items\": {\n            \"uniqueItems\": true,\n            \"minItems\": 1,\n            \"type\": \"string\"\n          },\n          \"default\": [\n            \"LightGreen\",\n            \"LightBlue\",\n            \"LightRed\",\n            \"Cyan\",\n            \"Green\",\n            \"Blue\",\n            \"Red\"\n          ],\n          \"description\": \"Represents the colour the GPU will use in the memory legend and graph\",\n          \"type\": \"array\"\n        },\n        \"rx_color\": {\n          \"default\": \"LightCyan\",\n          \"description\": \"Represents the colour rx will use in the network legend and graph\",\n          \"type\": \"string\"\n        },\n        \"tx_color\": {\n          \"default\": \"LightGreen\",\n          \"description\": \"Represents the colour tx will use in the network legend and graph\",\n          \"type\": \"string\"\n        },\n        \"border_color\": {\n          \"default\": \"Gray\",\n          \"description\": \"Represents the colour of the border of unselected widgets\",\n          \"type\": \"string\"\n        },\n        \"highlighted_border_color\": {\n          \"default\": \"LightBlue\",\n          \"description\": \"Represents the colour of the border of selected widgets\",\n          \"type\": \"string\"\n        },\n        \"text_color\": {\n          \"default\": \"Gray\",\n          \"description\": \"Represents the colour of most text\",\n          \"type\": \"string\"\n        },\n        \"selected_text_color\": {\n          \"default\": \"Black\",\n          \"description\": \"Represents the colour of text that is selected\",\n          \"type\": \"string\"\n        },\n        \"selected_bg_color\": {\n          \"default\": \"LightBlue\",\n          \"description\": \"Represents the background colour of text that is selected\",\n          \"type\": \"string\"\n        },\n        \"graph_color\": {\n          \"default\": \"Gray\",\n          \"description\": \"Represents the colour of the lines and text of the graph\",\n          \"type\": \"string\"\n        },\n        \"high_battery_color\": {\n          \"default\": \"green\",\n          \"description\": \"Represents the colours of the battery based on charge\",\n          \"type\": \"string\"\n        },\n        \"medium_battery_color\": {\n          \"default\": \"yellow\",\n          \"description\": \"Represents the colours of the battery based on charge\",\n          \"type\": \"string\"\n        },\n        \"low_battery_color\": {\n          \"default\": \"red\",\n          \"description\": \"Represents the colours of the battery based on charge\",\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"row\": {\n      \"$ref\": \"#/definitions/row\"\n    },\n    \"disk_filter\": {\n      \"$ref\": \"#/definitions/filter\"\n    },\n    \"mount_filter\": {\n      \"$ref\": \"#/definitions/filter\"\n    },\n    \"temp_filter\": {\n      \"$ref\": \"#/definitions/filter\"\n    },\n    \"net_filter\": {\n      \"$ref\": \"#/definitions/filter\"\n    }\n  }\n}\n"
  },
  {
    "path": "scripts/ci/bsd_tests.sh",
    "content": "#!/bin/sh\n\n# Script to be run by the `ci.yml` workflow for -BSD jobs based on the target.\n\nset -eu\n\nBSD_TARGET=\"${1:-}\"\n\nif [ -z \"$BSD_TARGET\" ]; then\n    echo \"Error: BSD target must be specified.\"\n    exit 1\nfi\n\nif [ \"$BSD_TARGET\" = \"x86_64-unknown-freebsd\" ]; then\n    pkg install -y curl bash\n    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs --output rustup.sh\n    sh rustup.sh --default-toolchain stable -y\n\n    . \"$HOME/.cargo/env\"\n    cargo test --no-fail-fast --locked -- --nocapture --quiet\nelif [ \"$BSD_TARGET\" = \"x86_64-unknown-netbsd\" ]; then\n    /usr/sbin/pkg_add -u curl bash mozilla-rootcerts-openssl\n    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs --output rustup.sh\n    sh rustup.sh --default-toolchain stable -y\n\n    . \"$HOME/.cargo/env\"\n    # TODO: Support default features eventually?\n    cargo test --no-fail-fast --locked --no-default-features -- --nocapture --quiet --skip test_data_collection\nelse\n    echo \"Unsupported BSD target type.\"\n    exit 1\nfi\n"
  },
  {
    "path": "scripts/ci/ci_bsd.sh",
    "content": "#!/bin/sh\n\n# Script to be run by the `ci.yml` workflow for -BSD jobs based on the target.\n\nset -eu\n\nBSD_TARGET=\"${1:-}\"\n\nif [ -z \"$BSD_TARGET\" ]; then\n    echo \"Error: BSD target must be specified.\"\n    exit 1\nfi\n\nif [ \"$BSD_TARGET\" = \"x86_64-unknown-openbsd\" ]; then\n    pkg_add rust rust-rustfmt\n\n    . \"$HOME/.cargo/env\"\n    cargo fmt --all -- --check\n    # Note this only tests the default features, but I think that's fine.\n    # We also do not run clippy because OpenBSD tends to lag behind due to\n    # it being tier 3 (see https://github.com/eza-community/eza/pull/1669).\n    cargo test --no-fail-fast --locked --no-default-features -- --nocapture --quiet\nelse\n    echo \"Unsupported BSD VM target type.\"\n    exit 1\nfi\n"
  },
  {
    "path": "scripts/ci/cirrus_release.py",
    "content": "#!/bin/python3\n\n# A simple script to trigger Cirrus CI builds and download the release artifacts\n# through Cirrus CI's GraphQL interface.\n#\n# Expects the Cirrus CI API key to be set in the CIRRUS_KEY environment variable.\n#\n# TODO: Explain this in docs how the heck this works.\n\nimport os\nimport json\nimport sys\nimport traceback\nfrom textwrap import dedent\nfrom time import sleep, time\nfrom pathlib import Path\nfrom typing import List, Optional, Tuple\n\nfrom urllib.request import Request, urlopen, urlretrieve\n\n# Form of each task is (TASK_ALIAS, FILE_NAME).\nTASKS: List[Tuple[str, str]] = [\n    (\"linux_2_17_build\", \"bottom_x86_64-unknown-linux-gnu-2-17.tar.gz\"),\n]\nURL = \"https://api.cirrus-ci.com/graphql\"\nDL_URL_TEMPLATE = \"https://api.cirrus-ci.com/v1/artifact/build/%s/%s/binaries/%s\"\n\n\ndef make_query_request(key: str, branch: str, mutation_id: str):\n    print(\"Creating query request.\")\n\n    # Dumb but if it works...\n    config_override = (\n        Path(\".cirrus.yml\")\n        .read_text()\n        .replace(\"# -PLACEHOLDER FOR CI-\", 'BTM_BUILD_RELEASE_CALLER: \"ci\"')\n    )\n\n    query = \"\"\"\n        mutation CreateCirrusCIBuild (\n            $repo: ID!,\n            $branch: String!,\n            $mutation_id: String!,\n            $config_override: String,\n        ) {\n            createBuild (\n                input: {\n                    repositoryId: $repo,\n                    branch: $branch,\n                    clientMutationId: $mutation_id,\n                    configOverride: $config_override\n                }\n            ) {\n                build {\n                    id,\n                    status\n                }\n            }\n        }\n    \"\"\"\n\n    params = {\n        \"repo\": \"6646638922956800\",\n        \"branch\": branch,\n        \"mutation_id\": mutation_id,\n        \"config_override\": dedent(config_override),\n    }\n\n    data = {\"query\": dedent(query), \"variables\": params}\n    data = json.dumps(data).encode()\n\n    request = Request(URL, data=data, method=\"POST\")\n    request.add_header(\"Authorization\", \"Bearer {}\".format(key))\n\n    return request\n\n\ndef check_build_status(key: str, build_id: str) -> Optional[str]:\n    query = \"\"\"\n        query BuildStatus($id: ID!) {\n            build(id: $id) {\n                status\n            }\n        }\n    \"\"\"\n\n    params = {\n        \"id\": build_id,\n    }\n\n    data = {\"query\": dedent(query), \"variables\": params}\n    data = json.dumps(data).encode()\n\n    request = Request(URL, data=data, method=\"POST\")\n    request.add_header(\"Authorization\", \"Bearer {}\".format(key))\n    with urlopen(request) as response:\n        response = json.load(response)\n        if response.get(\"errors\") is not None:\n            print(\"There was an error in the returned response.\")\n            return None\n\n        try:\n            status = response[\"data\"][\"build\"][\"status\"]\n            return status\n        except KeyError:\n            print(\"There was an issue with checking the build status.\")\n            return None\n\n\ndef check_build_tasks(key: str, build_id: str) -> Optional[List[str]]:\n    query = \"\"\"\n        query Build($id:ID!) {\n            build(id:$id){\n                tasks {\n                    id\n                }\n            }\n        }\n    \"\"\"\n\n    params = {\n        \"id\": build_id,\n    }\n\n    data = {\"query\": dedent(query), \"variables\": params}\n    data = json.dumps(data).encode()\n\n    request = Request(URL, data=data, method=\"POST\")\n    request.add_header(\"Authorization\", \"Bearer {}\".format(key))\n    with urlopen(request) as response:\n        response = json.load(response)\n\n        if response.get(\"errors\") is not None:\n            print(\"There was an error in the returned response.\")\n            return None\n\n        try:\n            tasks = [task[\"id\"] for task in response[\"data\"][\"build\"][\"tasks\"]]\n            return tasks\n        except KeyError:\n            print(\"There was an issue with getting the list of task ids.\")\n            return None\n\n\ndef stop_build_tasks(key: str, task_ids: List[str], mutation_id: str) -> bool:\n    query = \"\"\"\n        mutation StopCirrusCiTasks (\n            $task_ids: [ID!]!,\n            $mutation_id: String!,\n        ) {\n                batchAbort (\n                input: {\n                        taskIds: $task_ids,\n                        clientMutationId: $mutation_id\n                }\n            ) {\n                tasks {\n                    id\n                }\n            }\n        }\n    \"\"\"\n\n    params = {\n        \"task_ids\": task_ids,\n        \"mutation_id\": mutation_id,\n    }\n\n    data = {\"query\": dedent(query), \"variables\": params}\n    data = json.dumps(data).encode()\n\n    request = Request(URL, data=data, method=\"POST\")\n    request.add_header(\"Authorization\", \"Bearer {}\".format(key))\n\n    with urlopen(request) as response:\n        response = json.load(response)\n        return len(response[\"data\"][\"batchAbort\"][\"tasks\"]) == len(task_ids)\n\n\ndef try_download(build_id: str, dl_path: Path):\n    for task, file in TASKS:\n        url = DL_URL_TEMPLATE % (build_id, task, file)\n        out = os.path.join(dl_path, file)\n        print(f\"Downloading {file} to {out}\")\n        urlretrieve(url, out)\n\n\ndef main():\n    args = sys.argv\n    env = os.environ\n\n    key = env[\"CIRRUS_KEY\"]\n    branch = args[1]\n    dl_path = args[2] if len(args) >= 3 else \"\"\n    dl_path = Path(dl_path)\n    build_type = args[3] if len(args) >= 4 else \"build\"\n    build_id = args[4] if len(args) >= 5 else None\n\n    print(f\"Running Cirrus script with branch '{branch}'\")\n\n    # Check if this build has already been completed before.\n    if build_id is not None:\n        print(\"Previous build ID was provided, checking if complete.\")\n        status = check_build_status(key, build_id)\n        if status.startswith(\"COMPLETE\"):\n            print(\"Starting download of previous build ID\")\n            try_download(build_id, dl_path)\n    else:\n        # Try up to three times\n        MAX_ATTEMPTS = 5\n        success = False\n        tasks = []\n        mutation_id = None\n\n        for i in range(MAX_ATTEMPTS):\n            if success:\n                break\n\n            print(f\"Attempt {i + 1}:\")\n\n            if tasks and mutation_id:\n                print(\"Killing previous tasks first...\")\n\n                if stop_build_tasks(key, tasks, mutation_id):\n                    print(\"All previous tasks successfully stopped.\")\n                else:\n                    print(\n                        \"Not all previous tasks stopped. This isn't a problem but it is a waste.\"\n                    )\n\n            tasks = []\n            mutation_id = \"Cirrus CI Build {}-{}-{}\".format(\n                build_type, branch, int(time())\n            )\n\n            with urlopen(make_query_request(key, branch, mutation_id)) as response:\n                response = json.load(response)\n                errors = response.get(\"errors\")\n\n                if errors is not None:\n                    print(f\"There was an error in the returned response: {str(errors)}\")\n                    continue\n\n                try:\n                    build_id = response[\"data\"][\"createBuild\"][\"build\"][\"id\"]\n                    print(f\"Created build job {build_id}.\")\n                except KeyError:\n                    print(\"There was an issue with creating a build job.\")\n                    continue\n\n                # First, sleep X minutes total, as it's unlikely it'll finish before then.\n                SLEEP_MINUTES = 4\n                print(f\"Sleeping for {SLEEP_MINUTES} minutes.\")\n\n                # Sleep and check for tasks out every 10 seconds\n                for _ in range(SLEEP_MINUTES * 6):\n                    sleep(10)\n                    if not tasks:\n                        tasks = check_build_tasks(key, build_id)\n\n                MINUTES = 10\n                SLEEP_SEC = 30\n                TRIES = int(MINUTES * (60 / SLEEP_SEC))  # Works out to 20 tries.\n\n                print(f\"Mandatory nap over. Checking for completion for {MINUTES} min.\")\n\n                for attempt in range(TRIES):\n                    print(\"Checking...\")\n                    try:\n                        status = check_build_status(key, build_id)\n                        if status.startswith(\"COMPLETE\"):\n                            print(\"Build complete. Downloading artifact files.\")\n                            sleep(5)\n                            try_download(build_id, dl_path)\n                            success = True\n                            break\n                        else:\n                            print(f\"Build status: {(status or 'unknown')}\")\n\n                            if status == \"ABORTED\":\n                                print(\"Build aborted, bailing.\")\n                                break\n                            elif status.lower().startswith(\"fail\"):\n                                print(\"Build failed, bailing.\")\n                                break\n                            elif attempt + 1 < TRIES:\n                                sleep(SLEEP_SEC)\n                    except Exception as ex:\n                        print(\"Unexpected error:\")\n                        print(ex)\n                        print(traceback.format_exc())\n                        # Sleep for a minute if something went wrong, just in case.\n                        sleep(60)\n                else:\n                    print(f\"Build failed to complete after {MINUTES} minutes, bailing.\")\n\n        if not success:\n            exit(2)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/ci/configure_git.sh",
    "content": "#!/bin/bash\n\ngit config --global user.name ${GIT_USER}\ngit config --global user.email ${GIT_EMAIL}\necho Name: $(git config --get user.name)\necho Email: $(git config --get user.email)\n"
  },
  {
    "path": "scripts/clear_cache.py",
    "content": "#!/bin/python3\n\n# A simple script to clean caches matching a PR ID.\n#\n# Expects a GitHub token in the environment variables as GITHUB_TOKEN.\n\nimport json\nimport os\nimport sys\nimport time\nfrom urllib.error import HTTPError, URLError\n\nfrom urllib.request import Request, urlopen\n\nURL = \"https://api.github.com/repos/ClementTsang/bottom/actions/caches\"\n\n\ndef cache_list_request(key):\n    request = Request(URL, method=\"GET\")\n    request.add_header(\"Accept\", \"application/vnd.github+json\")\n    request.add_header(\"Authorization\", \"Bearer {}\".format(key))\n    return request\n\n\ndef delete_cache_request(key, id):\n    request = Request(\"{}/{}\".format(URL, id), method=\"DELETE\")\n    request.add_header(\"Accept\", \"application/vnd.github+json\")\n    request.add_header(\"Authorization\", \"Bearer {}\".format(key))\n    return request\n\n\ndef main():\n    args = sys.argv\n    env = os.environ\n\n    key = env[\"GITHUB_TOKEN\"]\n    if args[1].isnumeric():\n        pr_id = int(args[1])\n        ref = \"refs/pull/{}/merge\".format(pr_id)\n\n        print(\"Clearing any caches generated by PR {}\".format(pr_id))\n        with urlopen(cache_list_request(key)) as response:\n            response = json.load(response)\n            caches = response[\"actions_caches\"]\n            for cache in caches:\n                if cache[\"ref\"] == ref:\n                    id = cache[\"id\"]\n                    try:\n                        print(\"Deleting ID {}...\".format(id))\n                        urlopen(delete_cache_request(key, id))\n                    except HTTPError as e:\n                        print(\"HTTPError with delete, error code {}.\".format(e.code))\n                    except URLError as _:\n                        print(\"URLError with delete.\")\n                    else:\n                        print(\"Successfully deleted cache ID {}!\".format(id))\n                    time.sleep(0.1)\n    elif args[1] == \"keep-main\" or args[1] == \"keep-master\":\n        print(\"Clearing all but default branch cache.\")\n        with urlopen(cache_list_request(key)) as response:\n            response = json.load(response)\n            caches = response[\"actions_caches\"]\n            for cache in caches:\n                if not (\"master\" in cache[\"ref\"] or \"main\" in cache[\"ref\"]):\n                    id = cache[\"id\"]\n                    try:\n                        print(\"Deleting ID {}...\".format(id))\n                        urlopen(delete_cache_request(key, id))\n                    except HTTPError as e:\n                        print(\"HTTPError with delete, error code {}.\".format(e.code))\n                    except URLError as _:\n                        print(\"URLError with delete.\")\n                    else:\n                        print(\"Successfully deleted cache ID {}!\".format(id))\n                    time.sleep(0.1)\n    elif args[1] == \"main\" or args[1] == \"master\" or args[1] == \"all\":\n        print(\"Clearing all caches.\")\n        with urlopen(cache_list_request(key)) as response:\n            response = json.load(response)\n            caches = response[\"actions_caches\"]\n            for cache in caches:\n                id = cache[\"id\"]\n                try:\n                    print(\"Deleting ID {}...\".format(id))\n                    urlopen(delete_cache_request(key, id))\n                except HTTPError as e:\n                    print(\"HTTPError with delete, error code {}.\".format(e.code))\n                except URLError as _:\n                    print(\"URLError with delete.\")\n                else:\n                    print(\"Successfully deleted cache ID {}!\".format(id))\n                time.sleep(0.1)\n    else:\n        print(f\"Skipping, given argument {args[1]}.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/hooks/README.md",
    "content": "# Git Hooks\n\nOptional pre-push git hook. These are checked again in CI anyway, but may make it easier to check for issues upfront pre-push.\n"
  },
  {
    "path": "scripts/hooks/pre-push",
    "content": "#!/bin/sh\n\nset -e\n\necho \"Running pre-push hook:\"\n\necho \"Executing: cargo fmt --all -- --check\"\ncargo fmt --all -- --check\n\necho \"Executing: cargo clippy --all-targets --workspace -- -D warnings\"\ncargo clippy --all-targets --workspace -- -D warnings\n"
  },
  {
    "path": "scripts/schema/bad_file.toml",
    "content": "[flags]\nhide_avg_cpu = 'bad'\n"
  },
  {
    "path": "scripts/schema/generate.sh",
    "content": "#!/bin/bash\n\nset -e\n\ncd \"$(dirname \"$0\")\";\ncd ../..\n\ncargo run --bin schema --features=\"generate_schema\" -- $1 > schema/v$1/bottom.json\n"
  },
  {
    "path": "scripts/schema/nightly.sh",
    "content": "#!/bin/bash\n\nset -e\n\ncd \"$(dirname \"$0\")\";\ncd ../..\n\ncargo run --bin schema --features=\"generate_schema\" > schema/nightly/bottom.json\n"
  },
  {
    "path": "scripts/schema/requirements.txt",
    "content": "jsonschema-rs == 0.32.1\n"
  },
  {
    "path": "scripts/schema/validator.py",
    "content": "#!/bin/python3\n\n# A simple script to validate that a schema is valid for a file.\n\nimport argparse\nimport tomllib\nimport jsonschema_rs\nimport re\nimport traceback\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Validates a file against a JSON schema\"\n    )\n    parser.add_argument(\n        \"-f\", \"--file\", type=str, required=True, help=\"The file to check.\"\n    )\n    parser.add_argument(\n        \"-s\", \"--schema\", type=str, required=True, help=\"The schema to use.\"\n    )\n    parser.add_argument(\n        \"--uncomment\",\n        required=False,\n        action=\"store_true\",\n        help=\"Uncomment the settings inside the file.\",\n    )\n    parser.add_argument(\n        \"--should_fail\",\n        required=False,\n        action=\"store_true\",\n        help=\"Whether the checked file should fail.\",\n    )\n    args = parser.parse_args()\n\n    file = args.file\n    schema = args.schema\n    should_fail = args.should_fail\n    uncomment = args.uncomment\n\n    with open(file, \"rb\") as f, open(schema) as s:\n        try:\n            validator = jsonschema_rs.validator_for(s.read())\n        except:\n            print(\"Couldn't create validator.\")\n            exit()\n\n        if uncomment:\n            read_file = f.read().decode(\"utf-8\")\n            read_file = re.sub(r\"^#([a-zA-Z\\[])\", r\"\\1\", read_file, flags=re.MULTILINE)\n            read_file = re.sub(\n                r\"^#(\\s\\s+)([a-zA-Z\\[])\", r\"\\2\", read_file, flags=re.MULTILINE\n            )\n            print(f\"uncommented file: \\n{read_file}\\n=====\\n\")\n\n            toml_str = tomllib.loads(read_file)\n        else:\n            toml_str = tomllib.load(f)\n\n        try:\n            validator.validate(toml_str)\n            if should_fail:\n                print(\"Fail! Should have errored.\")\n                exit(1)\n            else:\n                print(\"All good!\")\n        except jsonschema_rs.ValidationError as err:\n            print(f\"Caught error: `{err}`\")\n            print(traceback.format_exc())\n\n            if should_fail:\n                print(\"Caught error, good!\")\n            else:\n                print(\"Fail!\")\n                exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/windows/choco/bottom.nuspec.template",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Read this before creating packages: https://chocolatey.org/docs/create-packages -->\n<!-- It is especially important to read the above link to understand additional requirements when publishing packages to the community feed aka dot org (https://chocolatey.org/packages). -->\n\n<!-- Test your packages in a test environment: https://github.com/chocolatey/chocolatey-test-environment -->\n\n<!--\nThis is a nuspec. It mostly adheres to https://docs.nuget.org/create/Nuspec-Reference. Chocolatey uses a special version of NuGet.Core that allows us to do more than was initially possible. As such there are certain things to be aware of:\n\n* the package xmlns schema url may cause issues with nuget.exe\n* Any of the following elements can ONLY be used by choco tools - projectSourceUrl, docsUrl, mailingListUrl, bugTrackerUrl, packageSourceUrl, provides, conflicts, replaces \n* nuget.exe can still install packages with those elements but they are ignored. Any authoring tools or commands will error on those elements \n-->\n\n<!-- You can embed software files directly into packages, as long as you are not bound by distribution rights. -->\n<!-- * If you are an organization making private packages, you probably have no issues here -->\n<!-- * If you are releasing to the community feed, you need to consider distribution rights. -->\n<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->\n<package xmlns=\"http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd\">\n  <metadata>\n    <!-- == PACKAGE SPECIFIC SECTION == -->\n    <id>bottom</id>\n    <version>$version</version>\n\n    <!-- == SOFTWARE SPECIFIC SECTION == -->\n    <!-- This section is about the software itself -->\n    <title>bottom</title>\n    <authors>Clement Tsang</authors>\n    <projectUrl>https://github.com/ClementTsang/bottom</projectUrl>\n    <licenseUrl>https://github.com/ClementTsang/bottom/blob/main/LICENSE</licenseUrl>\n    <requireLicenseAcceptance>true</requireLicenseAcceptance>\n    <projectSourceUrl>https://github.com/ClementTsang/bottom</projectSourceUrl>\n    <packageSourceUrl>https://github.com/ClementTsang/choco-bottom</packageSourceUrl>\n    <docsUrl>https://bottom.pages.dev/stable</docsUrl>\n    <bugTrackerUrl>https://github.com/ClementTsang/bottom/issues</bugTrackerUrl>\n    <tags>cli cross-platform terminal top tui monitoring bottom btm</tags>\n    <summary>A customizable cross-platform graphical process/system monitor for the terminal.</summary>\n    <description>\n    A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows. Inspired by [gtop](https://github.com/aksakalli/gtop), [gotop](https://github.com/xxxserxxx/gotop) and [htop](https://github.com/htop-dev/htop).\n\n    **Usage**\n    To use, run `btm` in a terminal.\n    \n    For more documentation and usage information, go to the [official repo](https://github.com/ClementTsang/bottom).\n\n    Please report any issues with the Chocolatey package [here](https://github.com/ClementTsang/choco-bottom).\n    \n    **Note**\n    This package currently depends on Visual C++ Redistributable for Visual Studio 2015 https://chocolatey.org/packages/vcredist2015. \n    </description>\n    <releaseNotes>https://github.com/ClementTsang/bottom/releases/tag/$version/</releaseNotes>\n  </metadata>\n  <files>\n    <!-- this section controls what actually gets packaged into the Chocolatey package -->\n    <file src=\"tools\\**\" target=\"tools\" />\n    <!--Building from Linux? You may need this instead: <file src=\"tools/**\" target=\"tools\" />-->\n  </files>\n</package>\n"
  },
  {
    "path": "scripts/windows/choco/choco_packager.py",
    "content": "# Because choco is a special case and I'm too lazy to make my\n# packaging script robust enough, so whatever, hard-code time.\n\nimport hashlib\nimport sys\nfrom string import Template\nimport os\n\nargs = sys.argv\ndeployment_file_path_64 = args[1]\nversion = args[2]\nnuspec_template = args[3]\nps1_template = args[4]\ngenerated_nuspec = args[5]\ngenerated_ps1 = args[6]\ngenerated_ps1_dir = args[7]\n\nprint(\"Generating Chocolatey package for:\")\nprint(\"     64-bit: %s\" % deployment_file_path_64)\nprint(\"     VERSION: %s\" % version)\nprint(\"     NUSPEC TEMPLATE: %s\" % nuspec_template)\nprint(\"     PS1 TEMPLATE: %s\" % ps1_template)\nprint(\"     GENERATED NUSPEC: %s\" % generated_nuspec)\nprint(\"     GENERATED PS1: %s\" % generated_ps1)\nprint(\"     GENERATED PS1 DIR: %s\" % generated_ps1_dir)\n\nwith open(deployment_file_path_64, \"rb\") as deployment_file_64:\n    hash_64 = hashlib.sha1(deployment_file_64.read()).hexdigest()\n\n    print(\"Generated hash for 64-bit program: %s\" % str(hash_64))\n\n    with open(nuspec_template, \"r\") as template_file:\n        template = Template(template_file.read())\n        substitute = template.safe_substitute(version=version)\n        print(\"\\n================== Generated nuspec file ==================\\n\")\n        print(substitute)\n        print(\"\\n============================================================\\n\")\n\n        with open(generated_nuspec, \"w\") as generated_file:\n            generated_file.write(substitute)\n\n    os.makedirs(generated_ps1_dir)\n    with open(ps1_template, \"r\") as template_file:\n        template = Template(template_file.read())\n        substitute = template.safe_substitute(version=version, hash_64=hash_64)\n        print(\n            \"\\n================== Generated chocolatey-install file ==================\\n\"\n        )\n        print(substitute)\n        print(\"\\n============================================================\\n\")\n\n        with open(generated_ps1, \"w\") as generated_file:\n            generated_file.write(substitute)\n"
  },
  {
    "path": "scripts/windows/choco/chocolateyinstall.ps1.template",
    "content": "$ErrorActionPreference = 'Stop';\n$toolsDir   = \"$(Split-Path -parent $MyInvocation.MyCommand.Definition)\"\n$url        = 'https://github.com/ClementTsang/bottom/releases/download/$version/bottom_x86_64-pc-windows-msvc.zip'\n\n$packageArgs = @{\n  packageName   = $env:ChocolateyPackageName\n  softwareName  = 'bottom'\n  unzipLocation = $toolsDir\n  fileType      = 'exe'\n  url           = $url\n  checksum      = '$hash_64'\n  checksumType  = 'sha1'\n\n}\nInstall-ChocolateyZipPackage @packageArgs\n"
  },
  {
    "path": "src/app/data/mod.rs",
    "content": "//! How we manage data internally.\n\nmod time_series;\npub use time_series::{TimeSeriesData, Values};\n\nmod process;\npub use process::ProcessData;\n\nmod store;\npub use store::*;\n\nmod temperature;\npub use temperature::*;\n"
  },
  {
    "path": "src/app/data/process.rs",
    "content": "use std::{collections::BTreeMap, vec::Vec};\n\nuse nohash::IntMap;\n\nuse crate::collection::processes::{Pid, ProcessHarvest};\n\n#[derive(Clone, Debug, Default)]\npub struct ProcessData {\n    /// A PID to process data map.\n    pub process_harvest: BTreeMap<Pid, ProcessHarvest>,\n\n    /// A mapping between a process PID to any children process PIDs.\n    pub process_parent_mapping: IntMap<Pid, Vec<Pid>>,\n\n    /// PIDs corresponding to processes that have no parents.\n    pub orphan_pids: Vec<Pid>,\n}\n\nimpl ProcessData {\n    pub(super) fn ingest(&mut self, list_of_processes: Vec<ProcessHarvest>) {\n        self.process_parent_mapping.clear();\n\n        // Reverse as otherwise the pid mappings are in the wrong order.\n        list_of_processes.iter().rev().for_each(|process_harvest| {\n            if let Some(parent_pid) = process_harvest.parent_pid {\n                if let Some(entry) = self.process_parent_mapping.get_mut(&parent_pid) {\n                    entry.push(process_harvest.pid);\n                } else {\n                    self.process_parent_mapping\n                        .insert(parent_pid, vec![process_harvest.pid]);\n                }\n            }\n        });\n\n        self.process_parent_mapping.shrink_to_fit();\n\n        let process_pid_map = list_of_processes\n            .into_iter()\n            .map(|process| (process.pid, process))\n            .collect();\n        self.process_harvest = process_pid_map;\n\n        // We collect all processes that either:\n        // - Do not have a parent PID (that is, they are orphan processes)\n        // - Have a parent PID but we don't have the parent (we promote them as orphans)\n        self.orphan_pids = self\n            .process_harvest\n            .iter()\n            .filter_map(|(pid, process_harvest)| match process_harvest.parent_pid {\n                Some(parent_pid) if self.process_harvest.contains_key(&parent_pid) => None,\n                _ => Some(*pid),\n            })\n            .collect();\n    }\n}\n"
  },
  {
    "path": "src/app/data/store.rs",
    "content": "use std::{\n    time::{Duration, Instant},\n    vec::Vec,\n};\n\nuse super::{ProcessData, TimeSeriesData};\n#[cfg(feature = \"battery\")]\nuse crate::collection::batteries;\nuse crate::{\n    app::AppConfigFields,\n    collection::{Data, cpu, disks, memory::MemData, network},\n    utils::data_units::DataUnit,\n    widgets::{DiskWidgetData, TempWidgetData},\n};\n\n/// A collection of data. This is where we dump data into.\n///\n/// TODO: Maybe reduce visibility of internal data, make it only accessible through DataStore?\n#[derive(Debug, Clone)]\npub struct StoredData {\n    pub last_update_time: Instant, // FIXME: (points_rework_v1) we could be able to remove this with some more refactoring.\n    pub timeseries_data: TimeSeriesData,\n    pub network_harvest: network::NetworkHarvest,\n    pub ram_harvest: Option<MemData>,\n    pub swap_harvest: Option<MemData>,\n    #[cfg(not(target_os = \"windows\"))]\n    pub cache_harvest: Option<MemData>,\n    #[cfg(feature = \"zfs\")]\n    pub arc_harvest: Option<MemData>,\n    #[cfg(feature = \"gpu\")]\n    pub gpu_harvest: Vec<(String, MemData)>,\n    pub cpu_harvest: cpu::CpuHarvest,\n    pub load_avg_harvest: cpu::LoadAvgHarvest,\n    pub process_data: ProcessData,\n    /// TODO: (points_rework_v1) Might be a better way to do this without having to store here?\n    pub prev_io: Vec<(u64, u64)>,\n    pub disk_harvest: Vec<DiskWidgetData>,\n    pub temp_data: Vec<TempWidgetData>,\n    #[cfg(feature = \"battery\")]\n    pub battery_harvest: Vec<batteries::BatteryData>,\n}\n\nimpl Default for StoredData {\n    fn default() -> Self {\n        StoredData {\n            last_update_time: Instant::now(),\n            timeseries_data: TimeSeriesData::default(),\n            network_harvest: network::NetworkHarvest::default(),\n            ram_harvest: None,\n            #[cfg(not(target_os = \"windows\"))]\n            cache_harvest: None,\n            swap_harvest: None,\n            cpu_harvest: cpu::CpuHarvest::default(),\n            load_avg_harvest: cpu::LoadAvgHarvest::default(),\n            process_data: Default::default(),\n            prev_io: Vec::default(),\n            disk_harvest: Vec::default(),\n            temp_data: Vec::default(),\n            #[cfg(feature = \"battery\")]\n            battery_harvest: Vec::default(),\n            #[cfg(feature = \"zfs\")]\n            arc_harvest: None,\n            #[cfg(feature = \"gpu\")]\n            gpu_harvest: Vec::default(),\n        }\n    }\n}\n\nimpl StoredData {\n    pub fn reset(&mut self) {\n        *self = StoredData::default();\n    }\n\n    #[allow(\n        clippy::boxed_local,\n        reason = \"This avoids warnings on certain platforms (e.g. 32-bit).\"\n    )]\n    fn eat_data(&mut self, mut data: Box<Data>, settings: &AppConfigFields) {\n        let harvested_time = data.collection_time;\n\n        // We must adjust all the network values to their selected type (defaults to bits).\n        if matches!(settings.network_unit_type, DataUnit::Byte) {\n            if let Some(network) = &mut data.network {\n                network.rx /= 8;\n                network.tx /= 8;\n            }\n        }\n\n        if !settings.use_basic_mode {\n            self.timeseries_data.add(&data);\n        }\n\n        if let Some(network) = data.network {\n            self.network_harvest = network;\n        }\n\n        self.ram_harvest = data.memory;\n        self.swap_harvest = data.swap;\n\n        #[cfg(not(target_os = \"windows\"))]\n        {\n            self.cache_harvest = data.cache;\n        }\n\n        #[cfg(feature = \"zfs\")]\n        {\n            self.arc_harvest = data.arc;\n        }\n\n        #[cfg(feature = \"gpu\")]\n        if let Some(gpu) = data.gpu {\n            self.gpu_harvest = gpu;\n        }\n\n        if let Some(cpu) = data.cpu {\n            self.cpu_harvest = cpu;\n        }\n\n        if let Some(load_avg) = data.load_avg {\n            self.load_avg_harvest = load_avg;\n        }\n\n        self.temp_data = data\n            .temperature_sensors\n            .map(|sensors| {\n                sensors\n                    .into_iter()\n                    .map(|temp| TempWidgetData {\n                        sensor: temp.name,\n                        temperature: temp\n                            .temperature\n                            .map(|c| settings.temperature_type.convert_temp_unit(c)),\n                    })\n                    .collect()\n            })\n            .unwrap_or_default();\n\n        if let Some(disks) = data.disks {\n            if let Some(io) = data.io {\n                self.eat_disks(disks, io, harvested_time);\n            }\n        }\n\n        if let Some(list_of_processes) = data.list_of_processes {\n            self.process_data.ingest(list_of_processes);\n        }\n\n        #[cfg(feature = \"battery\")]\n        {\n            if let Some(list_of_batteries) = data.list_of_batteries {\n                self.battery_harvest = list_of_batteries;\n            }\n        }\n\n        // And we're done eating. Update time and push the new entry!\n        self.last_update_time = harvested_time;\n    }\n\n    fn eat_disks(\n        &mut self, disks: Vec<disks::DiskHarvest>, io: disks::IoHarvest, harvested_time: Instant,\n    ) {\n        let time_since_last_harvest = harvested_time\n            .duration_since(self.last_update_time)\n            .as_secs_f64();\n\n        self.disk_harvest.clear();\n\n        let prev_io_diff = disks.len().saturating_sub(self.prev_io.len());\n        self.prev_io.reserve(prev_io_diff);\n        self.prev_io.extend((0..prev_io_diff).map(|_| (0, 0)));\n\n        for (itx, device) in disks.into_iter().enumerate() {\n            let Some(checked_name) = ({\n                #[cfg(target_os = \"windows\")]\n                {\n                    match &device.volume_name {\n                        Some(volume_name) => Some(volume_name.as_str()),\n                        None => device.name.split('/').next_back(),\n                    }\n                }\n                #[cfg(not(target_os = \"windows\"))]\n                {\n                    #[cfg(feature = \"zfs\")]\n                    {\n                        if !device.name.starts_with('/') {\n                            Some(device.name.as_str()) // use the whole zfs\n                        // dataset name\n                        } else {\n                            device.name.split('/').next_back()\n                        }\n                    }\n                    #[cfg(not(feature = \"zfs\"))]\n                    {\n                        device.name.split('/').next_back()\n                    }\n                }\n            }) else {\n                continue;\n            };\n\n            let io_device = {\n                #[cfg(target_os = \"macos\")]\n                {\n                    use std::sync::OnceLock;\n\n                    use regex::Regex;\n\n                    // Must trim one level further for macOS!\n                    static DISK_REGEX: OnceLock<Regex> = OnceLock::new();\n\n                    #[expect(\n                        clippy::regex_creation_in_loops,\n                        reason = \"this is fine since it's done via a static OnceLock. In the future though, separate it out.\"\n                    )]\n                    if let Some(new_name) = DISK_REGEX\n                        .get_or_init(|| Regex::new(r\"disk\\d+\").expect(\"valid regex\"))\n                        .find(checked_name)\n                    {\n                        io.get(new_name.as_str())\n                    } else {\n                        None\n                    }\n                }\n                #[cfg(not(target_os = \"macos\"))]\n                {\n                    io.get(checked_name)\n                }\n            };\n\n            let (mut io_read_rate_bytes, mut io_write_rate_bytes) = (None, None);\n            if let Some(Some(io_device)) = io_device {\n                if let Some(prev_io) = self.prev_io.get_mut(itx) {\n                    io_read_rate_bytes = Some(\n                        ((io_device.read_bytes.saturating_sub(prev_io.0)) as f64\n                            / time_since_last_harvest)\n                            .round() as u64,\n                    );\n\n                    io_write_rate_bytes = Some(\n                        ((io_device.write_bytes.saturating_sub(prev_io.1)) as f64\n                            / time_since_last_harvest)\n                            .round() as u64,\n                    );\n\n                    *prev_io = (io_device.read_bytes, io_device.write_bytes);\n                }\n            }\n\n            let summed_total_bytes = match (device.used_space, device.free_space) {\n                (Some(used), Some(free)) => Some(used + free),\n                _ => None,\n            };\n\n            self.disk_harvest.push(DiskWidgetData {\n                name: device.name,\n                mount_point: device.mount_point,\n                free_bytes: device.free_space,\n                used_bytes: device.used_space,\n                total_bytes: device.total_space,\n                summed_total_bytes,\n                io_read_rate_bytes,\n                io_write_rate_bytes,\n            });\n        }\n    }\n}\n\n/// If we freeze data collection updates, we want to return a \"frozen\" copy\n/// of the data at the time, while still updating things in the background.\n#[derive(Default)]\npub enum FrozenState {\n    #[default]\n    NotFrozen,\n    Frozen(Box<StoredData>),\n}\n\n/// What data to share to other parts of the application.\n#[derive(Default)]\npub struct DataStore {\n    frozen_state: FrozenState,\n    main: StoredData,\n}\n\nimpl DataStore {\n    /// Toggle whether the [`DataState`] is frozen or not.\n    pub fn toggle_frozen(&mut self) {\n        match &self.frozen_state {\n            FrozenState::NotFrozen => {\n                self.frozen_state = FrozenState::Frozen(Box::new(self.main.clone()));\n            }\n            FrozenState::Frozen(_) => self.frozen_state = FrozenState::NotFrozen,\n        }\n    }\n\n    /// Return whether the [`DataState`] is frozen or not.\n    pub fn is_frozen(&self) -> bool {\n        matches!(self.frozen_state, FrozenState::Frozen(_))\n    }\n\n    /// Return a reference to the currently available data. Note that if the data is\n    /// in a frozen state, it will return the snapshot of data from when it was frozen.\n    pub fn get_data(&self) -> &StoredData {\n        match &self.frozen_state {\n            FrozenState::NotFrozen => &self.main,\n            FrozenState::Frozen(collected_data) => collected_data,\n        }\n    }\n\n    /// Eat data.\n    pub fn eat_data(&mut self, data: Box<Data>, settings: &AppConfigFields) {\n        self.main.eat_data(data, settings);\n    }\n\n    /// Clean data.\n    pub fn clean_data(&mut self, max_duration: Duration) {\n        self.main.timeseries_data.prune(max_duration);\n    }\n\n    /// Reset data state.\n    pub fn reset(&mut self) {\n        self.frozen_state = FrozenState::NotFrozen;\n        self.main = StoredData::default();\n    }\n}\n"
  },
  {
    "path": "src/app/data/temperature.rs",
    "content": "//! Code around temperature data.\n\nuse std::{fmt::Display, str::FromStr};\n\n#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)]\npub enum TemperatureType {\n    #[default]\n    Celsius,\n    Kelvin,\n    Fahrenheit,\n}\n\nimpl FromStr for TemperatureType {\n    type Err = String;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        match s {\n            \"fahrenheit\" | \"f\" => Ok(TemperatureType::Fahrenheit),\n            \"kelvin\" | \"k\" => Ok(TemperatureType::Kelvin),\n            \"celsius\" | \"c\" => Ok(TemperatureType::Celsius),\n            _ => Err(format!(\n                \"'{s}' is an invalid temperature type, use one of: [kelvin, k, celsius, c, fahrenheit, f].\"\n            )),\n        }\n    }\n}\n\nimpl TemperatureType {\n    /// Given a temperature in Celsius, convert it if necessary for a different\n    /// unit.\n    pub fn convert_temp_unit(&self, celsius: f32) -> TypedTemperature {\n        match self {\n            TemperatureType::Celsius => TypedTemperature::Celsius(celsius.ceil() as u32),\n            TemperatureType::Kelvin => TypedTemperature::Kelvin((celsius + 273.15).ceil() as u32),\n            TemperatureType::Fahrenheit => {\n                TypedTemperature::Fahrenheit(((celsius * (9.0 / 5.0)) + 32.0).ceil() as u32)\n            }\n        }\n    }\n}\n\n/// A temperature and its type.\n#[derive(Debug, PartialEq, Clone, Eq, PartialOrd, Ord)]\npub enum TypedTemperature {\n    Celsius(u32),\n    Kelvin(u32),\n    Fahrenheit(u32),\n}\n\nimpl Display for TypedTemperature {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            TypedTemperature::Celsius(val) => write!(f, \"{val}°C\"),\n            TypedTemperature::Kelvin(val) => write!(f, \"{val}K\"),\n            TypedTemperature::Fahrenheit(val) => write!(f, \"{val}°F\"),\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn temp_conversions() {\n        const TEMP: f32 = 100.0;\n\n        assert_eq!(\n            TemperatureType::Celsius.convert_temp_unit(TEMP),\n            TypedTemperature::Celsius(TEMP as u32),\n        );\n\n        assert_eq!(\n            TemperatureType::Kelvin.convert_temp_unit(TEMP),\n            TypedTemperature::Kelvin(373.15_f32.ceil() as u32)\n        );\n\n        assert_eq!(\n            TemperatureType::Fahrenheit.convert_temp_unit(TEMP),\n            TypedTemperature::Fahrenheit(212)\n        );\n    }\n}\n"
  },
  {
    "path": "src/app/data/time_series.rs",
    "content": "//! Time series data.\n\nuse std::{\n    cmp::Ordering,\n    time::{Duration, Instant},\n    vec::Vec,\n};\n\n#[cfg(feature = \"gpu\")]\nuse rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};\nuse timeless::data::ChunkedData;\n\nuse crate::collection::Data;\n\n/// Values corresponding to a time slice.\npub type Values = ChunkedData<f64>;\n\n/// Represents time series data in a chunked, deduped manner.\n///\n/// Properties:\n/// - Time in this manner is represented in a reverse-offset fashion from the current time.\n/// - All data is stored in SoA fashion.\n/// - Values are stored in a chunked format, which facilitates gaps in data collection if needed.\n/// - Additional metadata is stored to make data pruning over time easy.\n#[derive(Clone, Debug, Default)]\npub struct TimeSeriesData {\n    /// Time values.\n    ///\n    /// TODO: (points_rework_v1) Either store millisecond-level only or offsets only.\n    pub time: Vec<Instant>,\n\n    /// Network RX data.\n    pub rx: Values,\n\n    /// Network TX data.\n    pub tx: Values,\n\n    /// CPU data.\n    pub cpu: Vec<Values>,\n\n    /// RAM memory data.\n    pub ram: Values,\n\n    /// Swap data.\n    pub swap: Values,\n\n    #[cfg(not(target_os = \"windows\"))]\n    /// Cache data.\n    pub cache_mem: Values,\n\n    #[cfg(feature = \"zfs\")]\n    /// Arc data.\n    pub arc_mem: Values,\n\n    #[cfg(feature = \"gpu\")]\n    /// GPU memory data.\n    pub gpu_mem: HashMap<String, Values>,\n}\n\nimpl TimeSeriesData {\n    /// Add a new data point.\n    pub fn add(&mut self, data: &Data) {\n        self.time.push(data.collection_time);\n\n        if let Some(network) = &data.network {\n            self.rx.push(network.rx as f64);\n            self.tx.push(network.tx as f64);\n        } else {\n            self.rx.insert_break();\n            self.tx.insert_break();\n        }\n\n        if let Some(cpu) = &data.cpu {\n            match self.cpu.len().cmp(&cpu.len()) {\n                Ordering::Less => {\n                    let diff = cpu.len() - self.cpu.len();\n                    self.cpu.reserve_exact(diff);\n\n                    for _ in 0..diff {\n                        self.cpu.push(Default::default());\n                    }\n                }\n                Ordering::Greater => {\n                    let diff = self.cpu.len() - cpu.len();\n                    let offset = self.cpu.len() - diff;\n\n                    for curr in &mut self.cpu[offset..] {\n                        curr.insert_break();\n                    }\n                }\n                Ordering::Equal => {}\n            }\n\n            for (curr, new_data) in self.cpu.iter_mut().zip(cpu.iter()) {\n                curr.push(new_data.usage.into());\n            }\n        } else {\n            for c in &mut self.cpu {\n                c.insert_break();\n            }\n        }\n\n        if let Some(memory) = &data.memory {\n            self.ram.push(memory.percentage());\n        } else {\n            self.ram.insert_break();\n        }\n\n        if let Some(swap) = &data.swap {\n            self.swap.push(swap.percentage());\n        } else {\n            self.swap.insert_break();\n        }\n\n        #[cfg(not(target_os = \"windows\"))]\n        {\n            if let Some(cache) = &data.cache {\n                self.cache_mem.push(cache.percentage());\n            } else {\n                self.cache_mem.insert_break();\n            }\n        }\n\n        #[cfg(feature = \"zfs\")]\n        {\n            if let Some(arc) = &data.arc {\n                self.arc_mem.push(arc.percentage());\n            } else {\n                self.arc_mem.insert_break();\n            }\n        }\n\n        #[cfg(feature = \"gpu\")]\n        {\n            if let Some(gpu) = &data.gpu {\n                let mut not_visited = self\n                    .gpu_mem\n                    .keys()\n                    .map(String::to_owned)\n                    .collect::<HashSet<_>>();\n\n                for (name, new_data) in gpu {\n                    not_visited.remove(name);\n\n                    if !self.gpu_mem.contains_key(name) {\n                        self.gpu_mem\n                            .insert(name.to_string(), ChunkedData::default());\n                    }\n\n                    let curr = self\n                        .gpu_mem\n                        .get_mut(name)\n                        .expect(\"entry must exist as it was created above\");\n                    curr.push(new_data.percentage());\n                }\n\n                for nv in not_visited {\n                    if let Some(entry) = self.gpu_mem.get_mut(&nv) {\n                        entry.insert_break();\n                    }\n                }\n            } else {\n                for g in self.gpu_mem.values_mut() {\n                    g.insert_break();\n                }\n            }\n        }\n    }\n\n    /// Prune any data older than the given duration.\n    pub fn prune(&mut self, max_age: Duration) {\n        if self.time.is_empty() {\n            return;\n        }\n\n        let now = Instant::now();\n        let end = {\n            let partition_point = self\n                .time\n                .partition_point(|then| now.duration_since(*then) > max_age);\n\n            // Partition point returns the first index that does not match the predicate, so minus one.\n            if partition_point > 0 {\n                partition_point - 1\n            } else {\n                // If the partition point was 0, then it means all values are too new to be pruned.\n                // crate::info!(\"Skipping prune.\");\n                return;\n            }\n        };\n\n        // crate::info!(\"Pruning up to index {end}.\");\n\n        // Note that end here is _inclusive_.\n        self.time.drain(0..=end);\n        self.time.shrink_to_fit();\n\n        let _ = self.rx.prune_and_shrink_to_fit(end);\n        let _ = self.tx.prune_and_shrink_to_fit(end);\n\n        for cpu in &mut self.cpu {\n            let _ = cpu.prune_and_shrink_to_fit(end);\n        }\n\n        let _ = self.ram.prune_and_shrink_to_fit(end);\n        let _ = self.swap.prune_and_shrink_to_fit(end);\n\n        #[cfg(not(target_os = \"windows\"))]\n        let _ = self.cache_mem.prune_and_shrink_to_fit(end);\n\n        #[cfg(feature = \"zfs\")]\n        let _ = self.arc_mem.prune_and_shrink_to_fit(end);\n\n        #[cfg(feature = \"gpu\")]\n        {\n            self.gpu_mem.retain(|_, gpu| {\n                let _ = gpu.prune(end);\n\n                // Remove the entry if it is empty. We can always add it again later.\n                if gpu.no_elements() {\n                    false\n                } else {\n                    gpu.shrink_to_fit();\n                    true\n                }\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/app/filter.rs",
    "content": "use regex::Regex;\n\n/// Filters used by widgets to filter out certain entries.\n/// TODO: Move this out maybe?\n#[derive(Debug, Clone)]\npub struct Filter {\n    /// Whether the filter _accepts_ all entries that match `list`,\n    /// or _denies_ any entries that match it.\n    is_list_ignored: bool, // TODO: Maybe change to \"ignore_matches\"?\n\n    /// The list of regexes to match against. Whether it goes through\n    /// the filter or not depends on `is_list_ignored`.\n    list: Vec<Regex>,\n}\n\nimpl Filter {\n    /// Create a new filter.\n    #[inline]\n    pub(crate) fn new(ignore_matches: bool, list: Vec<Regex>) -> Self {\n        Self {\n            is_list_ignored: ignore_matches,\n            list,\n        }\n    }\n\n    /// Whether the filter should keep the entry or reject it.\n    #[inline]\n    pub(crate) fn should_keep(&self, entry: &str) -> bool {\n        if self.has_match(entry) {\n            // If a match is found, then if we wanted to ignore if we match, return false.\n            // If we want to keep if we match, return true. Thus, return the\n            // inverse of `is_list_ignored`.\n            !self.is_list_ignored\n        } else {\n            self.is_list_ignored\n        }\n    }\n\n    /// Whether there is a filter that matches the result.\n    #[inline]\n    pub(crate) fn has_match(&self, value: &str) -> bool {\n        self.list.iter().any(|regex| regex.is_match(value))\n    }\n\n    /// Whether entries matching the list should be ignored or kept.\n    #[inline]\n    pub(crate) fn ignore_matches(&self) -> bool {\n        self.is_list_ignored\n    }\n\n    /// Check a filter if it exists, otherwise accept if it is [`None`].\n    #[inline]\n    pub(crate) fn optional_should_keep(filter: &Option<Self>, entry: &str) -> bool {\n        filter\n            .as_ref()\n            .map(|f| f.should_keep(entry))\n            .unwrap_or(true)\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use regex::Regex;\n\n    use super::*;\n\n    /// Test based on the issue in <https://github.com/ClementTsang/bottom/pull/1037>.\n    #[test]\n    fn filter_is_list_ignored() {\n        let results = [\n            \"CPU socket temperature\",\n            \"wifi_0\",\n            \"motherboard temperature\",\n            \"amd gpu\",\n        ];\n\n        let ignore_true = Filter {\n            is_list_ignored: true,\n            list: vec![Regex::new(\"temperature\").unwrap()],\n        };\n\n        assert_eq!(\n            results\n                .into_iter()\n                .filter(|r| ignore_true.should_keep(r))\n                .collect::<Vec<_>>(),\n            vec![\"wifi_0\", \"amd gpu\"]\n        );\n\n        let ignore_false = Filter {\n            is_list_ignored: false,\n            list: vec![Regex::new(\"temperature\").unwrap()],\n        };\n\n        assert_eq!(\n            results\n                .into_iter()\n                .filter(|r| ignore_false.should_keep(r))\n                .collect::<Vec<_>>(),\n            vec![\"CPU socket temperature\", \"motherboard temperature\"]\n        );\n\n        let multi_true = Filter {\n            is_list_ignored: true,\n            list: vec![\n                Regex::new(\"socket\").unwrap(),\n                Regex::new(\"temperature\").unwrap(),\n            ],\n        };\n\n        assert_eq!(\n            results\n                .into_iter()\n                .filter(|r| multi_true.should_keep(r))\n                .collect::<Vec<_>>(),\n            vec![\"wifi_0\", \"amd gpu\"]\n        );\n\n        let multi_false = Filter {\n            is_list_ignored: false,\n            list: vec![\n                Regex::new(\"socket\").unwrap(),\n                Regex::new(\"temperature\").unwrap(),\n            ],\n        };\n\n        assert_eq!(\n            results\n                .into_iter()\n                .filter(|r| multi_false.should_keep(r))\n                .collect::<Vec<_>>(),\n            vec![\"CPU socket temperature\", \"motherboard temperature\"]\n        );\n    }\n}\n"
  },
  {
    "path": "src/app/layout_manager.rs",
    "content": "use std::collections::BTreeMap;\n\nuse tui::layout::Constraint;\n\nuse crate::{constants::DEFAULT_WIDGET_ID, options::OptionError};\n\n// Represents a start and end coordinate in some dimension.\ntype LineSegment = (u16, u16);\n\ntype WidgetMappings = (u16, BTreeMap<LineSegment, u64>);\ntype ColumnRowMappings = (u16, BTreeMap<LineSegment, WidgetMappings>);\ntype ColumnMappings = (u16, BTreeMap<LineSegment, ColumnRowMappings>);\n\n/// Represents a more usable representation of the layout, derived from the\n/// config.\n///\n/// FIXME: This is kinda gross. Ideally optimize out the hard-coded stuff.\n#[derive(Clone, Debug)]\npub struct BottomLayout {\n    pub rows: Vec<BottomRow>,\n    pub total_row_height_ratio: u16,\n}\n\ntrait Ratio {\n    fn ratio(&self) -> u16;\n}\n\nimpl Ratio for Constraint {\n    fn ratio(&self) -> u16 {\n        match self {\n            Constraint::Min(min) => std::cmp::max(*min, 1),\n            Constraint::Length(_) => 1,\n            Constraint::Fill(scaling) => *scaling,\n            _ => unreachable!(\"if this gets hit then you're refactoring layouts\"),\n        }\n    }\n}\n\nimpl BottomLayout {\n    pub fn get_movement_mappings(&mut self) {\n        #[expect(clippy::suspicious_operation_groupings)] // Have to enable this, clippy really doesn't like me doing this with tuples...\n        fn is_intersecting(a: LineSegment, b: LineSegment) -> bool {\n            a.0 >= b.0 && a.1 <= b.1\n                || a.1 >= b.1 && a.0 <= b.0\n                || a.0 <= b.0 && a.1 >= b.0\n                || a.0 >= b.0 && a.0 < b.1 && a.1 >= b.1\n        }\n\n        fn get_distance(target: LineSegment, candidate: LineSegment) -> u16 {\n            if candidate.0 < target.0 {\n                candidate.1 - target.0\n            } else if candidate.1 < target.1 {\n                candidate.1 - candidate.0\n            } else {\n                target.1 - candidate.0\n            }\n        }\n\n        // Now we need to create the correct mapping for moving from a specific\n        // widget to another\n        let mut layout_mapping: BTreeMap<LineSegment, ColumnMappings> = BTreeMap::new();\n        let mut total_height = 0;\n        for row in &self.rows {\n            let mut row_width = 0;\n            let mut row_mapping: BTreeMap<LineSegment, ColumnRowMappings> = BTreeMap::new();\n            let mut is_valid_row = false;\n            for col in &row.children {\n                let mut col_row_height = 0;\n                let mut col_mapping: BTreeMap<LineSegment, WidgetMappings> = BTreeMap::new();\n                let mut is_valid_col = false;\n\n                for col_row in &col.children {\n                    let mut widget_width = 0;\n                    let mut col_row_mapping: BTreeMap<LineSegment, u64> = BTreeMap::new();\n                    let mut is_valid_col_row = false;\n                    for widget in &col_row.children {\n                        let widget_ratio = widget\n                            .ratio_override\n                            .unwrap_or_else(|| widget.constraint.ratio());\n\n                        match widget.widget_type {\n                            BottomWidgetType::Empty => {}\n                            _ => {\n                                is_valid_col_row = true;\n                                col_row_mapping.insert(\n                                    (\n                                        widget_width * 100 / col_row.total_widget_ratio,\n                                        (widget_width + widget_ratio) * 100\n                                            / col_row.total_widget_ratio,\n                                    ),\n                                    widget.widget_id,\n                                );\n                            }\n                        }\n                        widget_width += widget_ratio;\n                    }\n                    if is_valid_col_row {\n                        col_mapping.insert(\n                            (\n                                col_row_height * 100 / col.total_col_row_ratio,\n                                (col_row_height + col_row.constraint.ratio()) * 100\n                                    / col.total_col_row_ratio,\n                            ),\n                            (col.total_col_row_ratio, col_row_mapping),\n                        );\n                        is_valid_col = true;\n                    }\n\n                    col_row_height += col_row.constraint.ratio();\n                }\n                if is_valid_col {\n                    row_mapping.insert(\n                        (\n                            row_width * 100 / row.total_col_ratio,\n                            (row_width + col.constraint.ratio()) * 100 / row.total_col_ratio,\n                        ),\n                        (row.total_col_ratio, col_mapping),\n                    );\n                    is_valid_row = true;\n                }\n\n                row_width += col.constraint.ratio();\n            }\n            if is_valid_row {\n                layout_mapping.insert(\n                    (\n                        total_height * 100 / self.total_row_height_ratio,\n                        (total_height + row.constraint.ratio()) * 100 / self.total_row_height_ratio,\n                    ),\n                    (self.total_row_height_ratio, row_mapping),\n                );\n            }\n            total_height += row.constraint.ratio();\n        }\n\n        // Now pass through a second time; this time we want to build up\n        // our neighbour profile.\n        let mut height_cursor = 0;\n        for row in &mut self.rows {\n            let mut col_cursor = 0;\n            let row_height_percentage_start = height_cursor * 100 / self.total_row_height_ratio;\n            let row_height_percentage_end =\n                (height_cursor + row.constraint.ratio()) * 100 / self.total_row_height_ratio;\n\n            for col in &mut row.children {\n                let mut col_row_cursor = 0;\n                let col_width_percentage_start = col_cursor * 100 / row.total_col_ratio;\n                let col_width_percentage_end =\n                    (col_cursor + col.constraint.ratio()) * 100 / row.total_col_ratio;\n\n                for col_row in &mut col.children {\n                    let mut widget_cursor = 0;\n                    let col_row_height_percentage_start =\n                        col_row_cursor * 100 / col.total_col_row_ratio;\n                    let col_row_height_percentage_end =\n                        (col_row_cursor + col_row.constraint.ratio()) * 100\n                            / col.total_col_row_ratio;\n                    let col_row_children_len = col_row.children.len();\n\n                    for widget in &mut col_row.children {\n                        // Bail if empty.\n                        if let BottomWidgetType::Empty = widget.widget_type {\n                            continue;\n                        }\n\n                        let widget_ratio = widget\n                            .ratio_override\n                            .unwrap_or_else(|| widget.constraint.ratio());\n\n                        let widget_width_percentage_start =\n                            widget_cursor * 100 / col_row.total_widget_ratio;\n                        let widget_width_percentage_end =\n                            (widget_cursor + widget_ratio) * 100 / col_row.total_widget_ratio;\n\n                        if let Some(current_row) = layout_mapping\n                            .get(&(row_height_percentage_start, row_height_percentage_end))\n                        {\n                            // First check for within the same col_row for left and right\n                            if let Some(current_col) = current_row\n                                .1\n                                .get(&(col_width_percentage_start, col_width_percentage_end))\n                            {\n                                if let Some(current_col_row) = current_col.1.get(&(\n                                    col_row_height_percentage_start,\n                                    col_row_height_percentage_end,\n                                )) {\n                                    if let Some(to_left_widget) = current_col_row\n                                        .1\n                                        .range(\n                                            ..(\n                                                widget_width_percentage_start,\n                                                widget_width_percentage_start,\n                                            ),\n                                        )\n                                        .next_back()\n                                    {\n                                        widget.left_neighbour = Some(*to_left_widget.1);\n                                    }\n\n                                    // Right\n                                    if let Some(to_right_neighbour) = current_col_row\n                                        .1\n                                        .range(\n                                            (\n                                                widget_width_percentage_end,\n                                                widget_width_percentage_end,\n                                            )..,\n                                        )\n                                        .next()\n                                    {\n                                        widget.right_neighbour = Some(*to_right_neighbour.1);\n                                    }\n                                }\n                            }\n\n                            if widget.left_neighbour.is_none() {\n                                if let Some(to_left_col) = current_row\n                                    .1\n                                    .range(\n                                        ..(col_width_percentage_start, col_width_percentage_start),\n                                    )\n                                    .next_back()\n                                {\n                                    // Check left in same row\n                                    let mut current_best_distance = 0;\n                                    let mut current_best_widget_id = widget.widget_id;\n\n                                    for widget_position in &(to_left_col.1).1 {\n                                        let candidate_start = (widget_position.0).0;\n                                        let candidate_end = (widget_position.0).1;\n\n                                        if is_intersecting(\n                                            (\n                                                col_row_height_percentage_start,\n                                                col_row_height_percentage_end,\n                                            ),\n                                            (candidate_start, candidate_end),\n                                        ) {\n                                            let candidate_distance = get_distance(\n                                                (\n                                                    col_row_height_percentage_start,\n                                                    col_row_height_percentage_end,\n                                                ),\n                                                (candidate_start, candidate_end),\n                                            );\n\n                                            if current_best_distance < candidate_distance {\n                                                if let Some(new_best_widget) =\n                                                    (widget_position.1).1.iter().next_back()\n                                                {\n                                                    current_best_distance = candidate_distance + 1;\n                                                    current_best_widget_id = *(new_best_widget.1);\n                                                }\n                                            }\n                                        }\n                                    }\n                                    if current_best_distance > 0 {\n                                        widget.left_neighbour = Some(current_best_widget_id);\n                                    }\n                                }\n                            }\n\n                            if widget.right_neighbour.is_none() {\n                                if let Some(to_right_col) = current_row\n                                    .1\n                                    .range((col_width_percentage_end, col_width_percentage_end)..)\n                                    .next()\n                                {\n                                    // Check right in same row\n                                    let mut current_best_distance = 0;\n                                    let mut current_best_widget_id = widget.widget_id;\n\n                                    for widget_position in &(to_right_col.1).1 {\n                                        let candidate_start = (widget_position.0).0;\n                                        let candidate_end = (widget_position.0).1;\n\n                                        if is_intersecting(\n                                            (\n                                                col_row_height_percentage_start,\n                                                col_row_height_percentage_end,\n                                            ),\n                                            (candidate_start, candidate_end),\n                                        ) {\n                                            let candidate_distance = get_distance(\n                                                (\n                                                    col_row_height_percentage_start,\n                                                    col_row_height_percentage_end,\n                                                ),\n                                                (candidate_start, candidate_end),\n                                            );\n\n                                            if current_best_distance < candidate_distance {\n                                                if let Some(new_best_widget) =\n                                                    (widget_position.1).1.iter().next()\n                                                {\n                                                    current_best_distance = candidate_distance + 1;\n                                                    current_best_widget_id = *(new_best_widget.1);\n                                                }\n                                            }\n                                        }\n                                    }\n                                    if current_best_distance > 0 {\n                                        widget.right_neighbour = Some(current_best_widget_id);\n                                    }\n                                }\n                            }\n\n                            // Check up/down within same row;\n                            // else check up/down with other rows\n                            if let Some(current_col) = current_row\n                                .1\n                                .get(&(col_width_percentage_start, col_width_percentage_end))\n                            {\n                                if let Some(to_up) = current_col\n                                    .1\n                                    .range(\n                                        ..(\n                                            col_row_height_percentage_start,\n                                            col_row_height_percentage_start,\n                                        ),\n                                    )\n                                    .next_back()\n                                {\n                                    // Now check each widget_width and pick the best\n                                    for candidate_widget in &(to_up.1).1 {\n                                        let mut current_best_distance = 0;\n                                        let mut current_best_widget_id = widget.widget_id;\n                                        if is_intersecting(\n                                            (\n                                                widget_width_percentage_start,\n                                                widget_width_percentage_end,\n                                            ),\n                                            ((candidate_widget.0).0, (candidate_widget.0).1),\n                                        ) {\n                                            let candidate_best_distance = get_distance(\n                                                (\n                                                    widget_width_percentage_start,\n                                                    widget_width_percentage_end,\n                                                ),\n                                                ((candidate_widget.0).0, (candidate_widget.0).1),\n                                            );\n\n                                            if current_best_distance < candidate_best_distance {\n                                                current_best_distance = candidate_best_distance + 1;\n                                                current_best_widget_id = *candidate_widget.1;\n                                            }\n                                        }\n\n                                        if current_best_distance > 0 {\n                                            widget.up_neighbour = Some(current_best_widget_id);\n                                        }\n                                    }\n                                } else {\n                                    for next_row_up in layout_mapping\n                                        .range(\n                                            ..(\n                                                row_height_percentage_start,\n                                                row_height_percentage_start,\n                                            ),\n                                        )\n                                        .rev()\n                                    {\n                                        let mut current_best_distance = 0;\n                                        let mut current_best_widget_id = widget.widget_id;\n                                        let (target_start_width, target_end_width) =\n                                            if col_row_children_len > 1 {\n                                                (\n                                                    col_width_percentage_start\n                                                        + widget_width_percentage_start\n                                                            * (col_width_percentage_end\n                                                                - col_width_percentage_start)\n                                                            / 100,\n                                                    col_width_percentage_start\n                                                        + widget_width_percentage_end\n                                                            * (col_width_percentage_end\n                                                                - col_width_percentage_start)\n                                                            / 100,\n                                                )\n                                            } else {\n                                                (\n                                                    col_width_percentage_start,\n                                                    col_width_percentage_end,\n                                                )\n                                            };\n\n                                        for col_position in &(next_row_up.1).1 {\n                                            if let Some(next_col_row) =\n                                                (col_position.1).1.iter().next_back()\n                                            {\n                                                let (candidate_col_start, candidate_col_end) =\n                                                    ((col_position.0).0, (col_position.0).1);\n                                                let candidate_difference =\n                                                    candidate_col_end - candidate_col_start;\n                                                for candidate_widget in &(next_col_row.1).1 {\n                                                    let candidate_start = candidate_col_start\n                                                        + (candidate_widget.0).0\n                                                            * candidate_difference\n                                                            / 100;\n                                                    let candidate_end = candidate_col_start\n                                                        + (candidate_widget.0).1\n                                                            * candidate_difference\n                                                            / 100;\n\n                                                    if is_intersecting(\n                                                        (target_start_width, target_end_width),\n                                                        (candidate_start, candidate_end),\n                                                    ) {\n                                                        let candidate_distance = get_distance(\n                                                            (target_start_width, target_end_width),\n                                                            (candidate_start, candidate_end),\n                                                        );\n\n                                                        if current_best_distance\n                                                            < candidate_distance\n                                                        {\n                                                            current_best_distance =\n                                                                candidate_distance + 1;\n                                                            current_best_widget_id =\n                                                                *(candidate_widget.1);\n                                                        }\n                                                    }\n                                                }\n                                            }\n                                        }\n\n                                        if current_best_distance > 0 {\n                                            widget.up_neighbour = Some(current_best_widget_id);\n                                            break;\n                                        }\n                                    }\n                                }\n\n                                if let Some(to_down) = current_col\n                                    .1\n                                    .range(\n                                        (\n                                            col_row_height_percentage_start + 1,\n                                            col_row_height_percentage_start + 1,\n                                        )..,\n                                    )\n                                    .next()\n                                {\n                                    for candidate_widget in &(to_down.1).1 {\n                                        let mut current_best_distance = 0;\n                                        let mut current_best_widget_id = widget.widget_id;\n                                        if is_intersecting(\n                                            (\n                                                widget_width_percentage_start,\n                                                widget_width_percentage_end,\n                                            ),\n                                            ((candidate_widget.0).0, (candidate_widget.0).1),\n                                        ) {\n                                            let candidate_best_distance = get_distance(\n                                                (\n                                                    widget_width_percentage_start,\n                                                    widget_width_percentage_end,\n                                                ),\n                                                ((candidate_widget.0).0, (candidate_widget.0).1),\n                                            );\n\n                                            if current_best_distance < candidate_best_distance {\n                                                current_best_distance = candidate_best_distance + 1;\n                                                current_best_widget_id = *candidate_widget.1;\n                                            }\n                                        }\n\n                                        if current_best_distance > 0 {\n                                            widget.down_neighbour = Some(current_best_widget_id);\n                                        }\n                                    }\n                                } else {\n                                    for next_row_down in layout_mapping.range(\n                                        (\n                                            row_height_percentage_start + 1,\n                                            row_height_percentage_start + 1,\n                                        )..,\n                                    ) {\n                                        let mut current_best_distance = 0;\n                                        let mut current_best_widget_id = widget.widget_id;\n                                        let (target_start_width, target_end_width) =\n                                            if col_row_children_len > 1 {\n                                                (\n                                                    col_width_percentage_start\n                                                        + widget_width_percentage_start\n                                                            * (col_width_percentage_end\n                                                                - col_width_percentage_start)\n                                                            / 100,\n                                                    col_width_percentage_start\n                                                        + widget_width_percentage_end\n                                                            * (col_width_percentage_end\n                                                                - col_width_percentage_start)\n                                                            / 100,\n                                                )\n                                            } else {\n                                                (\n                                                    col_width_percentage_start,\n                                                    col_width_percentage_end,\n                                                )\n                                            };\n\n                                        for col_position in &(next_row_down.1).1 {\n                                            if let Some(next_col_row) =\n                                                (col_position.1).1.iter().next()\n                                            {\n                                                let (candidate_col_start, candidate_col_end) =\n                                                    ((col_position.0).0, (col_position.0).1);\n                                                let candidate_difference =\n                                                    candidate_col_end - candidate_col_start;\n                                                for candidate_widget in &(next_col_row.1).1 {\n                                                    let candidate_start = candidate_col_start\n                                                        + (candidate_widget.0).0\n                                                            * candidate_difference\n                                                            / 100;\n                                                    let candidate_end = candidate_col_start\n                                                        + (candidate_widget.0).1\n                                                            * candidate_difference\n                                                            / 100;\n\n                                                    if is_intersecting(\n                                                        (target_start_width, target_end_width),\n                                                        (candidate_start, candidate_end),\n                                                    ) {\n                                                        let candidate_distance = get_distance(\n                                                            (target_start_width, target_end_width),\n                                                            (candidate_start, candidate_end),\n                                                        );\n\n                                                        if current_best_distance\n                                                            < candidate_distance\n                                                        {\n                                                            current_best_distance =\n                                                                candidate_distance + 1;\n                                                            current_best_widget_id =\n                                                                *(candidate_widget.1);\n                                                        }\n                                                    }\n                                                }\n                                            }\n                                        }\n\n                                        if current_best_distance > 0 {\n                                            widget.down_neighbour = Some(current_best_widget_id);\n                                            break;\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        widget_cursor += widget_ratio;\n                    }\n                    col_row_cursor += col_row.constraint.ratio();\n                }\n                col_cursor += col.constraint.ratio();\n            }\n            height_cursor += row.constraint.ratio();\n        }\n    }\n\n    pub fn init_basic_default(use_battery: bool) -> Self {\n        let table_widgets = if use_battery {\n            let disk_widget = BottomWidget::new(BottomWidgetType::Disk, 4)\n                .canvas_handled()\n                .up_neighbour(Some(100))\n                .left_neighbour(Some(8))\n                .right_neighbour(Some(DEFAULT_WIDGET_ID + 2));\n\n            let proc_sort = BottomWidget::new(BottomWidgetType::ProcSort, DEFAULT_WIDGET_ID + 2)\n                .canvas_handled()\n                .up_neighbour(Some(100))\n                .down_neighbour(Some(DEFAULT_WIDGET_ID + 1))\n                .left_neighbour(Some(4))\n                .right_neighbour(Some(DEFAULT_WIDGET_ID))\n                .ratio(1)\n                .parent_reflector(Some((WidgetDirection::Right, 2)));\n\n            let proc = BottomWidget::new(BottomWidgetType::Proc, DEFAULT_WIDGET_ID)\n                .canvas_handled()\n                .up_neighbour(Some(100))\n                .down_neighbour(Some(DEFAULT_WIDGET_ID + 1))\n                .left_neighbour(Some(DEFAULT_WIDGET_ID + 2))\n                .right_neighbour(Some(7))\n                .ratio(2);\n\n            let proc_search =\n                BottomWidget::new(BottomWidgetType::ProcSearch, DEFAULT_WIDGET_ID + 1)\n                    .canvas_handled()\n                    .up_neighbour(Some(DEFAULT_WIDGET_ID))\n                    .left_neighbour(Some(4))\n                    .right_neighbour(Some(7))\n                    .parent_reflector(Some((WidgetDirection::Up, 1)));\n\n            let temp = BottomWidget::new(BottomWidgetType::Temp, 7)\n                .canvas_handled()\n                .up_neighbour(Some(100))\n                .left_neighbour(Some(DEFAULT_WIDGET_ID))\n                .right_neighbour(Some(8));\n\n            let battery = BottomWidget::new(BottomWidgetType::Battery, 8)\n                .canvas_handled()\n                .up_neighbour(Some(100))\n                .left_neighbour(Some(7))\n                .right_neighbour(Some(4));\n\n            vec![\n                BottomCol::new(vec![BottomColRow::new(vec![disk_widget]).canvas_handled()])\n                    .canvas_handled(),\n                BottomCol::new(vec![\n                    BottomColRow::new(vec![proc_sort, proc])\n                        .canvas_handled()\n                        .total_widget_ratio(3),\n                    BottomColRow::new(vec![proc_search]).canvas_handled(),\n                ])\n                .canvas_handled(),\n                BottomCol::new(vec![BottomColRow::new(vec![temp]).canvas_handled()])\n                    .canvas_handled(),\n                BottomCol::new(vec![BottomColRow::new(vec![battery]).canvas_handled()])\n                    .canvas_handled(),\n            ]\n        } else {\n            let disk = BottomWidget::new(BottomWidgetType::Disk, 4)\n                .canvas_handled()\n                .up_neighbour(Some(100))\n                .left_neighbour(Some(7))\n                .right_neighbour(Some(DEFAULT_WIDGET_ID + 2));\n\n            let proc_sort = BottomWidget::new(BottomWidgetType::ProcSort, DEFAULT_WIDGET_ID + 2)\n                .canvas_handled()\n                .up_neighbour(Some(100))\n                .down_neighbour(Some(DEFAULT_WIDGET_ID + 1))\n                .left_neighbour(Some(4))\n                .right_neighbour(Some(DEFAULT_WIDGET_ID))\n                .parent_reflector(Some((WidgetDirection::Right, 2)));\n\n            let proc = BottomWidget::new(BottomWidgetType::Proc, DEFAULT_WIDGET_ID)\n                .canvas_handled()\n                .up_neighbour(Some(100))\n                .down_neighbour(Some(DEFAULT_WIDGET_ID + 1))\n                .left_neighbour(Some(DEFAULT_WIDGET_ID + 2))\n                .right_neighbour(Some(7));\n\n            let proc_search =\n                BottomWidget::new(BottomWidgetType::ProcSearch, DEFAULT_WIDGET_ID + 1)\n                    .canvas_handled()\n                    .up_neighbour(Some(DEFAULT_WIDGET_ID))\n                    .left_neighbour(Some(4))\n                    .right_neighbour(Some(7))\n                    .parent_reflector(Some((WidgetDirection::Up, 1)));\n\n            let temp = BottomWidget::new(BottomWidgetType::Temp, 7)\n                .canvas_handled()\n                .up_neighbour(Some(100))\n                .left_neighbour(Some(DEFAULT_WIDGET_ID))\n                .right_neighbour(Some(4));\n\n            vec![\n                BottomCol::new(vec![BottomColRow::new(vec![disk]).canvas_handled()])\n                    .canvas_handled(),\n                BottomCol::new(vec![\n                    BottomColRow::new(vec![proc_sort, proc]).canvas_handled(),\n                    BottomColRow::new(vec![proc_search]).canvas_handled(),\n                ])\n                .canvas_handled(),\n                BottomCol::new(vec![BottomColRow::new(vec![temp]).canvas_handled()])\n                    .canvas_handled(),\n            ]\n        };\n\n        let cpu = BottomWidget::new(BottomWidgetType::BasicCpu, 1)\n            .canvas_handled()\n            .down_neighbour(Some(2));\n\n        let mem = BottomWidget::new(BottomWidgetType::BasicMem, 2)\n            .canvas_handled()\n            .up_neighbour(Some(1))\n            .down_neighbour(Some(100))\n            .right_neighbour(Some(3));\n\n        let net = BottomWidget::new(BottomWidgetType::BasicNet, 3)\n            .canvas_handled()\n            .up_neighbour(Some(1))\n            .down_neighbour(Some(100))\n            .left_neighbour(Some(2));\n\n        let table = BottomWidget::new(BottomWidgetType::BasicTables, 100)\n            .canvas_handled()\n            .up_neighbour(Some(2));\n\n        BottomLayout {\n            total_row_height_ratio: 3,\n            rows: vec![\n                BottomRow::new(vec![\n                    BottomCol::new(vec![BottomColRow::new(vec![cpu]).canvas_handled()])\n                        .canvas_handled(),\n                ])\n                .canvas_handled(),\n                BottomRow::new(vec![\n                    BottomCol::new(vec![BottomColRow::new(vec![mem, net]).canvas_handled()])\n                        .canvas_handled(),\n                ])\n                .canvas_handled(),\n                BottomRow::new(vec![\n                    BottomCol::new(vec![BottomColRow::new(vec![table]).canvas_handled()])\n                        .canvas_handled(),\n                ])\n                .canvas_handled(),\n                BottomRow::new(table_widgets).canvas_handled(),\n            ],\n        }\n    }\n}\n\n/// Represents a single row in the layout.\n#[derive(Clone, Debug)]\npub struct BottomRow {\n    pub children: Vec<BottomCol>,\n    pub total_col_ratio: u16,\n    pub constraint: Constraint,\n}\n\nimpl BottomRow {\n    pub fn new(children: Vec<BottomCol>) -> Self {\n        Self {\n            children,\n            total_col_ratio: 1,\n            constraint: Constraint::Fill(1),\n        }\n    }\n\n    pub fn total_col_ratio(mut self, total_col_ratio: u16) -> Self {\n        self.total_col_ratio = total_col_ratio;\n        self\n    }\n\n    pub fn ratio(mut self, value: u16) -> Self {\n        self.constraint = Constraint::Fill(value);\n        self\n    }\n\n    pub fn canvas_handled(mut self) -> Self {\n        self.constraint = Constraint::Length(0);\n        self\n    }\n}\n\n/// Represents a single column in the layout.  We assume that even if the column\n/// contains only ONE element, it is still a column (rather than either a col or\n/// a widget, as per the config, for simplicity's sake).\n#[derive(Clone, Debug)]\npub struct BottomCol {\n    pub children: Vec<BottomColRow>,\n    pub total_col_row_ratio: u16,\n    pub constraint: Constraint,\n}\n\nimpl BottomCol {\n    pub fn new(children: Vec<BottomColRow>) -> Self {\n        Self {\n            children,\n            total_col_row_ratio: 1,\n            constraint: Constraint::Fill(1),\n        }\n    }\n\n    pub fn total_col_row_ratio(mut self, total_col_row_ratio: u16) -> Self {\n        self.total_col_row_ratio = total_col_row_ratio;\n        self\n    }\n\n    pub fn ratio(mut self, value: u16) -> Self {\n        self.constraint = Constraint::Fill(value);\n        self\n    }\n\n    pub fn canvas_handled(mut self) -> Self {\n        self.constraint = Constraint::Length(0);\n        self\n    }\n}\n\n#[derive(Clone, Default, Debug)]\npub struct BottomColRow {\n    pub children: Vec<BottomWidget>,\n    pub total_widget_ratio: u16,\n    pub constraint: Constraint,\n}\n\nimpl BottomColRow {\n    pub(crate) fn new(children: Vec<BottomWidget>) -> Self {\n        Self {\n            children,\n            total_widget_ratio: 1,\n            constraint: Constraint::Fill(1),\n        }\n    }\n\n    pub(crate) fn total_widget_ratio(mut self, total_widget_ratio: u16) -> Self {\n        self.total_widget_ratio = total_widget_ratio;\n        self\n    }\n\n    pub fn ratio(mut self, value: u16) -> Self {\n        self.constraint = Constraint::Fill(value);\n        self\n    }\n\n    pub fn canvas_handled(mut self) -> Self {\n        self.constraint = Constraint::Length(0);\n        self\n    }\n\n    pub fn grow(mut self, minimum: Option<u16>) -> Self {\n        self.constraint = Constraint::Min(minimum.unwrap_or(0));\n        self\n    }\n}\n\n#[derive(Debug, Clone, Eq, PartialEq)]\npub enum WidgetDirection {\n    Left,\n    Right,\n    Up,\n    Down,\n}\n\nimpl WidgetDirection {\n    pub fn is_opposite(&self, other_direction: &WidgetDirection) -> bool {\n        let to_compare = match &self {\n            WidgetDirection::Left => WidgetDirection::Right,\n            WidgetDirection::Right => WidgetDirection::Left,\n            WidgetDirection::Up => WidgetDirection::Down,\n            WidgetDirection::Down => WidgetDirection::Up,\n        };\n\n        *other_direction == to_compare\n    }\n}\n\n/// Represents a single widget.\n#[derive(Debug, Default, Clone)]\npub struct BottomWidget {\n    pub widget_type: BottomWidgetType,\n    pub widget_id: u64,\n    pub constraint: Constraint,\n    pub left_neighbour: Option<u64>,\n    pub right_neighbour: Option<u64>,\n    pub up_neighbour: Option<u64>,\n    pub down_neighbour: Option<u64>,\n\n    /// The value is the direction to bounce, as well as the parent offset.\n    pub parent_reflector: Option<(WidgetDirection, u64)>,\n\n    /// Top left corner when drawn, for mouse click detection. (x, y)\n    ///\n    /// TODO: Replace this with just an Option<Rect> for top + bottom.\n    pub top_left_corner: Option<(u16, u16)>,\n\n    /// Bottom right corner when drawn, for mouse click detection. (x, y)\n    pub bottom_right_corner: Option<(u16, u16)>,\n\n    /// TODO: REMOVE THIS LATER. This is temporary code to bridge the\n    /// old layout system with a newer system later.\n    ratio_override: Option<u16>,\n}\n\nimpl BottomWidget {\n    pub(crate) fn new(widget_type: BottomWidgetType, widget_id: u64) -> Self {\n        Self {\n            widget_type,\n            widget_id,\n            constraint: Constraint::Fill(1),\n            left_neighbour: None,\n            right_neighbour: None,\n            up_neighbour: None,\n            down_neighbour: None,\n            parent_reflector: None,\n            top_left_corner: None,\n            bottom_right_corner: None,\n            ratio_override: None,\n        }\n    }\n\n    pub(crate) fn left_neighbour(mut self, left_neighbour: Option<u64>) -> Self {\n        self.left_neighbour = left_neighbour;\n        self\n    }\n\n    pub(crate) fn right_neighbour(mut self, right_neighbour: Option<u64>) -> Self {\n        self.right_neighbour = right_neighbour;\n        self\n    }\n\n    pub(crate) fn up_neighbour(mut self, up_neighbour: Option<u64>) -> Self {\n        self.up_neighbour = up_neighbour;\n        self\n    }\n\n    pub(crate) fn down_neighbour(mut self, down_neighbour: Option<u64>) -> Self {\n        self.down_neighbour = down_neighbour;\n        self\n    }\n\n    pub(crate) fn ratio(mut self, value: u16) -> Self {\n        self.constraint = Constraint::Fill(value);\n        self\n    }\n\n    pub fn canvas_handled(mut self) -> Self {\n        self.constraint = Constraint::Length(0);\n        self\n    }\n\n    pub fn grow(mut self, minimum: Option<u16>) -> Self {\n        self.constraint = Constraint::Min(minimum.unwrap_or(0));\n        self\n    }\n\n    /// TODO: REMOVE THIS LATER. This is temporary code to bridge the\n    /// old layout system with a newer system later.\n    pub fn with_ratio_override(mut self, ratio_override: u16) -> Self {\n        self.ratio_override = Some(ratio_override);\n        self\n    }\n\n    pub(crate) fn parent_reflector(\n        mut self, parent_reflector: Option<(WidgetDirection, u64)>,\n    ) -> Self {\n        self.parent_reflector = parent_reflector;\n        self\n    }\n}\n\n#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]\npub enum BottomWidgetType {\n    #[default]\n    Empty,\n    Cpu,\n    CpuLegend,\n    Mem,\n    Net,\n    Proc,\n    ProcSearch,\n    ProcSort,\n    Temp,\n    Disk,\n    BasicCpu,\n    BasicMem,\n    BasicNet,\n    BasicTables,\n    Battery,\n}\n\nimpl BottomWidgetType {\n    pub fn is_widget_table(&self) -> bool {\n        use BottomWidgetType::*;\n        matches!(self, Disk | Proc | ProcSort | Temp | CpuLegend)\n    }\n\n    pub fn is_widget_graph(&self) -> bool {\n        use BottomWidgetType::*;\n        matches!(self, Cpu | Net | Mem)\n    }\n\n    pub fn get_pretty_name(&self) -> &str {\n        use BottomWidgetType::*;\n        match self {\n            Cpu => \"CPU\",\n            Mem => \"Memory\",\n            Net => \"Network\",\n            Proc => \"Processes\",\n            Temp => \"Temperature\",\n            Disk => \"Disks\",\n            Battery => \"Battery\",\n            _ => \"\",\n        }\n    }\n}\n\nimpl std::str::FromStr for BottomWidgetType {\n    type Err = OptionError;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        let lower_case = s.to_lowercase();\n        match lower_case.as_str() {\n            \"cpu\" => Ok(BottomWidgetType::Cpu),\n            \"mem\" | \"memory\" => Ok(BottomWidgetType::Mem),\n            \"net\" | \"network\" => Ok(BottomWidgetType::Net),\n            \"proc\" | \"process\" | \"processes\" => Ok(BottomWidgetType::Proc),\n            \"temp\" | \"temperature\" => Ok(BottomWidgetType::Temp),\n            \"disk\" => Ok(BottomWidgetType::Disk),\n            \"empty\" => Ok(BottomWidgetType::Empty),\n            #[cfg(feature = \"battery\")]\n            \"battery\" | \"batt\" => Ok(BottomWidgetType::Battery),\n            _ => {\n                #[cfg(feature = \"battery\")]\n                {\n                    Err(OptionError::config(format!(\n                        \"'{s}' is an invalid widget name.\n        \nSupported widget names:\n+--------------------------+\n|            cpu           |\n+--------------------------+\n|        mem, memory       |\n+--------------------------+\n|       net, network       |\n+--------------------------+\n| proc, process, processes |\n+--------------------------+\n|     temp, temperature    |\n+--------------------------+\n|           disk           |\n+--------------------------+\n|       batt, battery      |\n+--------------------------+\n|           empty          |\n+--------------------------+\n                \",\n                    )))\n                }\n                #[cfg(not(feature = \"battery\"))]\n                {\n                    Err(OptionError::config(format!(\n                        \"'{s}' is an invalid widget name.\n\nSupported widget names:\n+--------------------------+\n|            cpu           |\n+--------------------------+\n|        mem, memory       |\n+--------------------------+\n|       net, network       |\n+--------------------------+\n| proc, process, processes |\n+--------------------------+\n|     temp, temperature    |\n+--------------------------+\n|           disk           |\n+--------------------------+\n|           empty          |\n+--------------------------+\n                \",\n                    )))\n                }\n            }\n        }\n    }\n}\n\n#[derive(Clone, Default, Debug, Copy)]\npub struct UsedWidgets {\n    pub use_cpu: bool,\n    pub use_mem: bool,\n    pub use_cache: bool,\n    pub use_gpu: bool,\n    pub use_net: bool,\n    pub use_proc: bool,\n    pub use_disk: bool,\n    pub use_temp: bool,\n    pub use_battery: bool,\n}\n"
  },
  {
    "path": "src/app/states.rs",
    "content": "use std::ops::Range;\n\nuse indexmap::IndexMap;\nuse rustc_hash::FxHashMap as HashMap;\nuse unicode_ellipsis::grapheme_width;\nuse unicode_segmentation::{GraphemeCursor, GraphemeIncomplete, UnicodeSegmentation};\n\nuse crate::{\n    app::layout_manager::BottomWidgetType,\n    constants,\n    widgets::{\n        BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState,\n        ProcWidgetState, TempWidgetState, query::ProcessQuery,\n    },\n};\n\npub struct AppWidgetStates {\n    pub cpu_state: CpuState,\n    pub mem_state: MemState,\n    pub net_state: NetState,\n    pub proc_state: ProcState,\n    pub temp_state: TempState,\n    pub disk_state: DiskState,\n    pub battery_state: AppBatteryState,\n    pub basic_table_widget_state: Option<BasicTableWidgetState>,\n}\n\n#[derive(Debug)]\npub enum CursorDirection {\n    Left,\n    Right,\n}\n\npub struct AppHelpDialogState {\n    pub is_showing_help: bool,\n    pub height: u16,\n    pub scroll_state: ParagraphScrollState,\n    pub index_shortcuts: Vec<u16>,\n}\n\nimpl Default for AppHelpDialogState {\n    fn default() -> Self {\n        AppHelpDialogState {\n            is_showing_help: false,\n            height: 0,\n            scroll_state: ParagraphScrollState::default(),\n            index_shortcuts: vec![0; constants::HELP_TEXT.len()],\n        }\n    }\n}\n\n/// AppSearchState deals with generic searching (I might do this in the future).\npub struct AppSearchState {\n    pub is_enabled: bool,\n    pub current_search_query: String,\n    pub is_blank_search: bool,\n    pub is_invalid_search: bool,\n    pub grapheme_cursor: GraphemeCursor,\n    pub cursor_direction: CursorDirection,\n\n    pub display_start_char_index: usize,\n    pub size_mappings: IndexMap<usize, Range<usize>>,\n\n    /// The query. TODO: Merge this as one enum.\n    pub query: Option<ProcessQuery>,\n    pub error_message: Option<String>,\n}\n\nimpl Default for AppSearchState {\n    fn default() -> Self {\n        AppSearchState {\n            is_enabled: false,\n            current_search_query: String::default(),\n            is_invalid_search: false,\n            is_blank_search: true,\n            grapheme_cursor: GraphemeCursor::new(0, 0, true),\n            cursor_direction: CursorDirection::Right,\n            display_start_char_index: 0,\n            size_mappings: IndexMap::default(),\n            query: None,\n            error_message: None,\n        }\n    }\n}\n\nimpl AppSearchState {\n    /// Resets the [`AppSearchState`] to its default state, albeit still\n    /// enabled.\n    pub fn reset(&mut self) {\n        *self = AppSearchState {\n            is_enabled: self.is_enabled,\n            ..AppSearchState::default()\n        }\n    }\n\n    /// Returns whether the [`AppSearchState`] has an invalid or blank search.\n    pub fn is_invalid_or_blank_search(&self) -> bool {\n        self.is_blank_search || self.is_invalid_search\n    }\n\n    /// Sets the starting grapheme index to draw from.\n    pub fn get_start_position(&mut self, available_width: usize, is_force_redraw: bool) {\n        // Remember - the number of columns != the number of grapheme slots/sizes, you\n        // cannot use index to determine this reliably!\n\n        let start_index = if is_force_redraw {\n            0\n        } else {\n            self.display_start_char_index\n        };\n        let cursor_index = self.grapheme_cursor.cur_cursor();\n\n        if let Some(start_range) = self.size_mappings.get(&start_index) {\n            let cursor_range = self\n                .size_mappings\n                .get(&cursor_index)\n                .cloned()\n                .unwrap_or_else(|| {\n                    self.size_mappings\n                        .last()\n                        .map(|(_, r)| r.end..(r.end + 1))\n                        .unwrap_or(start_range.end..(start_range.end + 1))\n                });\n\n            // Cases to handle in both cases:\n            // - The current start index can show the cursor's word.\n            // - The current start index cannot show the cursor's word.\n            //\n            // What differs is how we \"scroll\" based on the cursor movement direction.\n\n            self.display_start_char_index = match self.cursor_direction {\n                CursorDirection::Right => {\n                    if start_range.start + available_width >= cursor_range.end {\n                        // Use the current index.\n                        start_index\n                    } else if cursor_range.end >= available_width {\n                        // If the current position is past the last visible element, skip until we\n                        // see it.\n\n                        let mut index = 0;\n                        for i in 0..(cursor_index + 1) {\n                            if let Some(r) = self.size_mappings.get(&i) {\n                                if r.start + available_width >= cursor_range.end {\n                                    index = i;\n                                    break;\n                                }\n                            }\n                        }\n\n                        index\n                    } else {\n                        0\n                    }\n                }\n                CursorDirection::Left => {\n                    if cursor_range.start < start_range.end {\n                        let mut index = 0;\n                        for i in cursor_index..(self.current_search_query.len()) {\n                            if let Some(r) = self.size_mappings.get(&i) {\n                                if r.start + available_width >= cursor_range.end {\n                                    index = i;\n                                    break;\n                                }\n                            }\n                        }\n                        index\n                    } else {\n                        start_index\n                    }\n                }\n            };\n        } else {\n            // If we fail here somehow, just reset to 0 index + scroll left.\n            self.display_start_char_index = 0;\n            self.cursor_direction = CursorDirection::Left;\n        };\n    }\n\n    pub(crate) fn walk_forward(&mut self) {\n        // TODO: Add tests for this.\n        let start_position = self.grapheme_cursor.cur_cursor();\n        let chunk = &self.current_search_query[start_position..];\n\n        match self.grapheme_cursor.next_boundary(chunk, start_position) {\n            Ok(_) => {}\n            Err(err) => match err {\n                GraphemeIncomplete::PreContext(ctx) => {\n                    // Provide the entire string as context. Not efficient but should resolve\n                    // failures.\n                    self.grapheme_cursor\n                        .provide_context(&self.current_search_query[0..ctx], 0);\n\n                    self.grapheme_cursor\n                        .next_boundary(chunk, start_position)\n                        .expect(\"another grapheme boundary should exist after the cursor with the provided context\");\n                }\n                _ => panic!(\"{err:?}\"),\n            },\n        }\n    }\n\n    pub(crate) fn walk_backward(&mut self) {\n        // TODO: Add tests for this.\n        let start_position = self.grapheme_cursor.cur_cursor();\n        let chunk = &self.current_search_query[..start_position];\n\n        match self.grapheme_cursor.prev_boundary(chunk, 0) {\n            Ok(_) => {}\n            Err(err) => match err {\n                GraphemeIncomplete::PreContext(ctx) => {\n                    // Provide the entire string as context. Not efficient but should resolve\n                    // failures.\n                    self.grapheme_cursor\n                        .provide_context(&self.current_search_query[0..ctx], 0);\n\n                    self.grapheme_cursor\n                        .prev_boundary(chunk, 0)\n                        .expect(\"another grapheme boundary should exist before the cursor with the provided context\");\n                }\n                _ => panic!(\"{err:?}\"),\n            },\n        }\n    }\n\n    pub(crate) fn update_sizes(&mut self) {\n        self.size_mappings.clear();\n        let mut curr_offset = 0;\n        for (index, grapheme) in\n            UnicodeSegmentation::grapheme_indices(self.current_search_query.as_str(), true)\n        {\n            let width = grapheme_width(grapheme);\n            let end = curr_offset + width;\n\n            self.size_mappings.insert(index, curr_offset..end);\n\n            curr_offset = end;\n        }\n\n        self.size_mappings.shrink_to_fit();\n    }\n}\n\npub struct ProcState {\n    pub widget_states: HashMap<u64, ProcWidgetState>,\n}\n\nimpl ProcState {\n    pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self {\n        ProcState { widget_states }\n    }\n\n    pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> {\n        self.widget_states.get_mut(&widget_id)\n    }\n\n    pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> {\n        self.widget_states.get(&widget_id)\n    }\n}\n\npub struct NetState {\n    pub widget_states: HashMap<u64, NetWidgetState>,\n}\n\nimpl NetState {\n    pub fn init(widget_states: HashMap<u64, NetWidgetState>) -> Self {\n        NetState { widget_states }\n    }\n}\n\npub struct CpuState {\n    pub widget_states: HashMap<u64, CpuWidgetState>,\n}\n\nimpl CpuState {\n    pub fn init(widget_states: HashMap<u64, CpuWidgetState>) -> Self {\n        CpuState { widget_states }\n    }\n\n    pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut CpuWidgetState> {\n        self.widget_states.get_mut(&widget_id)\n    }\n\n    pub fn get_widget_state(&self, widget_id: u64) -> Option<&CpuWidgetState> {\n        self.widget_states.get(&widget_id)\n    }\n}\n\npub struct MemState {\n    pub widget_states: HashMap<u64, MemWidgetState>,\n}\n\nimpl MemState {\n    pub fn init(widget_states: HashMap<u64, MemWidgetState>) -> Self {\n        MemState { widget_states }\n    }\n}\n\npub struct TempState {\n    pub widget_states: HashMap<u64, TempWidgetState>,\n}\n\nimpl TempState {\n    pub fn init(widget_states: HashMap<u64, TempWidgetState>) -> Self {\n        TempState { widget_states }\n    }\n\n    pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut TempWidgetState> {\n        self.widget_states.get_mut(&widget_id)\n    }\n\n    pub fn get_widget_state(&self, widget_id: u64) -> Option<&TempWidgetState> {\n        self.widget_states.get(&widget_id)\n    }\n}\n\npub struct DiskState {\n    pub widget_states: HashMap<u64, DiskTableWidget>,\n}\n\nimpl DiskState {\n    pub fn init(widget_states: HashMap<u64, DiskTableWidget>) -> Self {\n        DiskState { widget_states }\n    }\n\n    pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut DiskTableWidget> {\n        self.widget_states.get_mut(&widget_id)\n    }\n\n    pub fn get_widget_state(&self, widget_id: u64) -> Option<&DiskTableWidget> {\n        self.widget_states.get(&widget_id)\n    }\n}\npub struct BasicTableWidgetState {\n    // Since this is intended (currently) to only be used for ONE widget, that's\n    // how it's going to be written.  If we want to allow for multiple of these,\n    // then we can expand outwards with a normal BasicTableState and a hashmap\n    pub currently_displayed_widget_type: BottomWidgetType,\n    pub currently_displayed_widget_id: u64,\n    pub left_tlc: Option<(u16, u16)>,\n    pub left_brc: Option<(u16, u16)>,\n    pub right_tlc: Option<(u16, u16)>,\n    pub right_brc: Option<(u16, u16)>,\n}\n\npub struct AppBatteryState {\n    pub widget_states: HashMap<u64, BatteryWidgetState>,\n}\n\nimpl AppBatteryState {\n    pub fn init(widget_states: HashMap<u64, BatteryWidgetState>) -> Self {\n        AppBatteryState { widget_states }\n    }\n\n    pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut BatteryWidgetState> {\n        self.widget_states.get_mut(&widget_id)\n    }\n}\n\n#[derive(Default)]\npub struct ParagraphScrollState {\n    pub current_scroll_index: u16,\n    pub max_scroll_index: u16,\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    fn move_right(state: &mut AppSearchState) {\n        state.walk_forward();\n        state.cursor_direction = CursorDirection::Right;\n    }\n\n    fn move_left(state: &mut AppSearchState) {\n        state.walk_backward();\n        state.cursor_direction = CursorDirection::Left;\n    }\n\n    #[test]\n    fn search_cursor_moves() {\n        let mut state = AppSearchState::default();\n        state.current_search_query = \"Hi, 你好! 🇦🇶\".to_string();\n        state.grapheme_cursor = GraphemeCursor::new(0, state.current_search_query.len(), true);\n        state.update_sizes();\n\n        // Moving right.\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 0);\n        assert_eq!(state.display_start_char_index, 0);\n\n        move_right(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 1);\n        assert_eq!(state.display_start_char_index, 0);\n\n        move_right(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 2);\n        assert_eq!(state.display_start_char_index, 0);\n\n        move_right(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 3);\n        assert_eq!(state.display_start_char_index, 0);\n\n        move_right(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 4);\n        assert_eq!(state.display_start_char_index, 2);\n\n        move_right(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 7);\n        assert_eq!(state.display_start_char_index, 4);\n\n        move_right(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 10);\n        assert_eq!(state.display_start_char_index, 7);\n\n        move_right(&mut state);\n        move_right(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 12);\n        assert_eq!(state.display_start_char_index, 10);\n\n        // Moving left.\n        move_left(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 11);\n        assert_eq!(state.display_start_char_index, 10);\n\n        move_left(&mut state);\n        move_left(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 7);\n        assert_eq!(state.display_start_char_index, 7);\n\n        move_left(&mut state);\n        move_left(&mut state);\n        move_left(&mut state);\n        move_left(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 1);\n        assert_eq!(state.display_start_char_index, 1);\n\n        move_left(&mut state);\n        state.get_start_position(4, false);\n        assert_eq!(state.grapheme_cursor.cur_cursor(), 0);\n        assert_eq!(state.display_start_char_index, 0);\n    }\n}\n"
  },
  {
    "path": "src/app.rs",
    "content": "pub mod data;\npub mod filter;\npub mod layout_manager;\npub mod states;\n\nuse std::time::Instant;\n\nuse concat_string::concat_string;\nuse data::*;\nuse filter::*;\nuse layout_manager::*;\nuse rustc_hash::FxHashMap as HashMap;\npub use states::*;\nuse unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};\n\nuse crate::{\n    canvas::{\n        components::time_graph::LegendPosition, dialogs::process_kill_dialog::ProcessKillDialog,\n    },\n    constants,\n    utils::data_units::DataUnit,\n    widgets::{ProcWidgetColumn, ProcWidgetMode, TreeCollapsed},\n};\n\nconst STALE_MIN_MILLISECONDS: u64 = 30 * 1000; // Lowest is 30 seconds\n\n#[derive(Debug, Clone, Eq, PartialEq, Default, Copy)]\npub enum AxisScaling {\n    #[default]\n    Log,\n    Linear,\n}\n\n/// AppConfigFields is meant to cover basic fields that would normally be set\n/// by config files or launch options.\n#[derive(Debug, Default, Eq, PartialEq)]\npub struct AppConfigFields {\n    pub update_rate: u64,\n    pub temperature_type: TemperatureType,\n    pub use_dot: bool,\n    pub cpu_left_legend: bool,\n    pub show_average_cpu: bool, // TODO: Unify this in CPU options\n    pub use_current_cpu_total: bool,\n    pub unnormalized_cpu: bool,\n    pub get_process_threads: bool,\n    pub use_basic_mode: bool,\n    pub default_time_value: u64,\n    pub time_interval: u64,\n    pub hide_time: bool,\n    pub autohide_time: bool,\n    pub use_old_network_legend: bool,\n    pub table_gap: u16,\n    pub disable_click: bool,\n    pub disable_keys: bool,\n    pub enable_gpu: bool,\n    pub enable_cache_memory: bool,\n    pub show_table_scroll_position: bool,\n    #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n    pub is_advanced_kill: bool,\n    pub is_read_only: bool,\n    #[cfg(target_os = \"linux\")]\n    pub hide_k_threads: bool,\n    #[cfg(feature = \"zfs\")]\n    pub free_arc: bool,\n    pub memory_legend_position: Option<LegendPosition>,\n    // TODO: Remove these, move network details state-side.\n    pub network_unit_type: DataUnit,\n    pub network_legend_position: Option<LegendPosition>,\n    pub network_scale_type: AxisScaling,\n    pub network_use_binary_prefix: bool,\n    pub network_show_packets: bool,\n    pub retention_ms: u64,\n    pub dedicated_average_row: bool,\n    pub default_tree_collapse: bool,\n}\n\n/// For filtering out information\n#[derive(Debug, Clone)]\npub struct DataFilters {\n    pub disk_filter: Option<Filter>,\n    pub mount_filter: Option<Filter>,\n    pub temp_filter: Option<Filter>,\n    pub net_filter: Option<Filter>,\n}\n\npub struct App {\n    awaiting_second_char: bool,\n    second_char: Option<char>,\n    pub data_store: DataStore,\n    last_key_press: Instant,\n    pub(crate) process_kill_dialog: ProcessKillDialog,\n    pub help_dialog_state: AppHelpDialogState,\n    pub is_expanded: bool,\n    pub is_force_redraw: bool,\n    pub is_determining_widget_boundary: bool,\n    pub basic_mode_use_percent: bool,\n    pub states: AppWidgetStates,\n    pub app_config_fields: AppConfigFields,\n    pub widget_map: HashMap<u64, BottomWidget>,\n    pub current_widget: BottomWidget,\n    pub used_widgets: UsedWidgets,\n    pub filters: DataFilters,\n}\n\nimpl App {\n    /// Create a new [`App`].\n    pub fn new(\n        app_config_fields: AppConfigFields, states: AppWidgetStates,\n        widget_map: HashMap<u64, BottomWidget>, current_widget: BottomWidget,\n        used_widgets: UsedWidgets, filters: DataFilters, is_expanded: bool,\n    ) -> Self {\n        Self {\n            awaiting_second_char: false,\n            second_char: None,\n            data_store: DataStore::default(),\n            last_key_press: Instant::now(),\n            process_kill_dialog: ProcessKillDialog::default(),\n            help_dialog_state: AppHelpDialogState::default(),\n            is_expanded,\n            is_force_redraw: false,\n            is_determining_widget_boundary: false,\n            basic_mode_use_percent: false,\n            states,\n            app_config_fields,\n            widget_map,\n            current_widget,\n            used_widgets,\n            filters,\n        }\n    }\n\n    /// Update the data in the [`App`].\n    pub fn update_data(&mut self) {\n        let data_source = self.data_store.get_data();\n\n        // FIXME: (points_rework_v1) maybe separate PR but would it make more sense to store references of data?\n        // Would it also make more sense to move the \"data set\" step to the draw step, and make it only set if force\n        // update is set here?\n        for proc in self.states.proc_state.widget_states.values_mut() {\n            if proc.force_update_data {\n                proc.set_table_data(data_source);\n            }\n        }\n\n        for temp in self.states.temp_state.widget_states.values_mut() {\n            if temp.force_update_data {\n                temp.set_table_data(&data_source.temp_data);\n            }\n        }\n\n        for cpu in self.states.cpu_state.widget_states.values_mut() {\n            if cpu.force_update_data {\n                cpu.set_legend_data(&data_source.cpu_harvest);\n            }\n        }\n\n        for disk in self.states.disk_state.widget_states.values_mut() {\n            if disk.force_update_data {\n                disk.set_table_data(data_source);\n            }\n        }\n    }\n\n    pub fn reset(&mut self) {\n        // Reset multi\n        self.reset_multi_tap_keys();\n\n        // Reset dialog state\n        self.help_dialog_state.is_showing_help = false;\n        self.process_kill_dialog.reset();\n\n        // Close all searches and reset it\n        self.states\n            .proc_state\n            .widget_states\n            .values_mut()\n            .for_each(|state| {\n                state.proc_search.search_state.reset();\n            });\n\n        self.data_store.reset();\n\n        // Reset zoom\n        self.reset_cpu_zoom();\n        self.reset_mem_zoom();\n        self.reset_net_zoom();\n    }\n\n    pub fn should_get_widget_bounds(&self) -> bool {\n        self.is_force_redraw || self.is_determining_widget_boundary\n    }\n\n    pub fn on_esc(&mut self) {\n        self.reset_multi_tap_keys();\n\n        if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.on_esc();\n            self.is_force_redraw = true;\n        } else if self.help_dialog_state.is_showing_help {\n            self.help_dialog_state.is_showing_help = false;\n            self.help_dialog_state.scroll_state.current_scroll_index = 0;\n            self.is_force_redraw = true;\n        } else {\n            match self.current_widget.widget_type {\n                BottomWidgetType::Proc => {\n                    if let Some(pws) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        if pws.is_search_enabled() || pws.is_sort_open {\n                            pws.proc_search.search_state.is_enabled = false;\n                            pws.is_sort_open = false;\n                            self.is_force_redraw = true;\n                            return;\n                        }\n                    }\n                }\n                BottomWidgetType::ProcSearch => {\n                    if let Some(pws) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id - 1)\n                    {\n                        if pws.is_search_enabled() {\n                            pws.proc_search.search_state.is_enabled = false;\n                            self.move_widget_selection(&WidgetDirection::Up);\n                            self.is_force_redraw = true;\n                            return;\n                        }\n                    }\n                }\n                BottomWidgetType::ProcSort => {\n                    if let Some(pws) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id - 2)\n                    {\n                        if pws.is_sort_open {\n                            pws.is_sort_open = false;\n                            self.move_widget_selection(&WidgetDirection::Right);\n                            self.is_force_redraw = true;\n                            return;\n                        }\n                    }\n                }\n                _ => {}\n            }\n\n            if self.is_expanded {\n                self.is_expanded = false;\n                self.is_force_redraw = true;\n            }\n        }\n    }\n\n    pub fn is_in_search_widget(&self) -> bool {\n        matches!(\n            self.current_widget.widget_type,\n            BottomWidgetType::ProcSearch\n        )\n    }\n\n    fn reset_multi_tap_keys(&mut self) {\n        self.awaiting_second_char = false;\n        self.second_char = None;\n    }\n\n    fn is_in_dialog(&self) -> bool {\n        self.help_dialog_state.is_showing_help || self.process_kill_dialog.is_open()\n    }\n\n    fn ignore_normal_keybinds(&self) -> bool {\n        self.is_in_dialog()\n    }\n\n    pub fn on_tab(&mut self) {\n        // Allow usage whilst only in processes\n\n        if !self.ignore_normal_keybinds() {\n            if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                if let Some(proc_widget_state) = self\n                    .states\n                    .proc_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    proc_widget_state.toggle_tab();\n                }\n            }\n        }\n    }\n\n    pub fn on_slash(&mut self) {\n        if !self.ignore_normal_keybinds() {\n            match &self.current_widget.widget_type {\n                BottomWidgetType::Proc | BottomWidgetType::ProcSort => {\n                    // Toggle on\n                    if let Some(proc_widget_state) = self.states.proc_state.get_mut_widget_state(\n                        self.current_widget.widget_id\n                            - match &self.current_widget.widget_type {\n                                BottomWidgetType::ProcSort => 2,\n                                _ => 0,\n                            },\n                    ) {\n                        proc_widget_state.proc_search.search_state.is_enabled = true;\n                        self.move_widget_selection(&WidgetDirection::Down);\n                        self.is_force_redraw = true;\n                    }\n                }\n                _ => {}\n            }\n        }\n    }\n\n    pub fn toggle_sort_menu(&mut self) {\n        let widget_id = self.current_widget.widget_id\n            - match &self.current_widget.widget_type {\n                BottomWidgetType::Proc => 0,\n                BottomWidgetType::ProcSort => 2,\n                _ => 0,\n            };\n\n        if let Some(pws) = self.states.proc_state.get_mut_widget_state(widget_id) {\n            pws.is_sort_open = !pws.is_sort_open;\n            pws.force_rerender = true;\n\n            // If the sort is now open, move left. Otherwise, if the proc sort was selected,\n            // force move right.\n            if pws.is_sort_open {\n                pws.sort_table.set_position(pws.table.sort_index());\n                self.move_widget_selection(&WidgetDirection::Left);\n            } else if let BottomWidgetType::ProcSort = self.current_widget.widget_type {\n                self.move_widget_selection(&WidgetDirection::Right);\n            }\n            self.is_force_redraw = true;\n        }\n    }\n\n    pub fn invert_sort(&mut self) {\n        match &self.current_widget.widget_type {\n            BottomWidgetType::Proc | BottomWidgetType::ProcSort => {\n                let widget_id = self.current_widget.widget_id\n                    - match &self.current_widget.widget_type {\n                        BottomWidgetType::Proc => 0,\n                        BottomWidgetType::ProcSort => 2,\n                        _ => 0,\n                    };\n\n                if let Some(pws) = self.states.proc_state.get_mut_widget_state(widget_id) {\n                    pws.table.toggle_order();\n                    pws.force_data_update();\n                }\n            }\n            _ => {}\n        }\n    }\n\n    pub fn toggle_percentages(&mut self) {\n        match &self.current_widget.widget_type {\n            BottomWidgetType::BasicMem => {\n                self.basic_mode_use_percent = !self.basic_mode_use_percent; // Oh god this is so lazy.\n            }\n            BottomWidgetType::Proc => {\n                if let Some(proc_widget_state) = self\n                    .states\n                    .proc_state\n                    .widget_states\n                    .get_mut(&self.current_widget.widget_id)\n                {\n                    proc_widget_state.toggle_mem_percentage();\n                }\n            }\n\n            _ => {}\n        }\n    }\n\n    pub fn toggle_ignore_case(&mut self) {\n        let is_in_search_widget = self.is_in_search_widget();\n        if let Some(proc_widget_state) = self\n            .states\n            .proc_state\n            .widget_states\n            .get_mut(&(self.current_widget.widget_id - 1))\n        {\n            if is_in_search_widget && proc_widget_state.is_search_enabled() {\n                proc_widget_state.proc_search.search_toggle_ignore_case();\n                proc_widget_state.update_query();\n            }\n        }\n    }\n\n    pub fn toggle_search_whole_word(&mut self) {\n        let is_in_search_widget = self.is_in_search_widget();\n        if let Some(proc_widget_state) = self\n            .states\n            .proc_state\n            .widget_states\n            .get_mut(&(self.current_widget.widget_id - 1))\n        {\n            if is_in_search_widget && proc_widget_state.is_search_enabled() {\n                proc_widget_state.proc_search.search_toggle_whole_word();\n                proc_widget_state.update_query();\n            }\n        }\n    }\n\n    pub fn toggle_search_regex(&mut self) {\n        let is_in_search_widget = self.is_in_search_widget();\n        if let Some(proc_widget_state) = self\n            .states\n            .proc_state\n            .widget_states\n            .get_mut(&(self.current_widget.widget_id - 1))\n        {\n            if is_in_search_widget && proc_widget_state.is_search_enabled() {\n                proc_widget_state.proc_search.search_toggle_regex();\n                proc_widget_state.update_query();\n            }\n        }\n    }\n\n    pub fn toggle_tree_mode(&mut self) {\n        if let Some(proc_widget_state) = self\n            .states\n            .proc_state\n            .widget_states\n            .get_mut(&(self.current_widget.widget_id))\n        {\n            match proc_widget_state.mode {\n                ProcWidgetMode::Tree { .. } => {\n                    proc_widget_state.mode = ProcWidgetMode::Normal;\n                    proc_widget_state.force_rerender_and_update();\n                }\n                ProcWidgetMode::Normal => {\n                    proc_widget_state.mode = ProcWidgetMode::Tree(TreeCollapsed::new(\n                        self.app_config_fields.default_tree_collapse,\n                    ));\n                    proc_widget_state.force_rerender_and_update();\n                }\n                ProcWidgetMode::Grouped => {}\n            }\n        }\n    }\n\n    /// One of two functions allowed to run while in a dialog...\n    pub fn on_enter(&mut self) {\n        if self.process_kill_dialog.is_open() {\n            // Not the best way of doing things for now but works as glue.\n            self.process_kill_dialog.on_enter();\n        } else if !self.is_in_dialog() {\n            match self.current_widget.widget_type {\n                BottomWidgetType::ProcSearch => {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id - 1)\n                    {\n                        if proc_widget_state.is_search_enabled() {\n                            proc_widget_state.proc_search.search_state.is_enabled = false;\n                            self.move_widget_selection(&WidgetDirection::Up);\n                            self.is_force_redraw = true;\n                        }\n                    }\n                }\n                BottomWidgetType::ProcSort => {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id - 2)\n                    {\n                        proc_widget_state.use_sort_table_value();\n                        if proc_widget_state.is_sort_open {\n                            proc_widget_state.is_sort_open = false;\n                            self.move_widget_selection(&WidgetDirection::Right);\n                            self.is_force_redraw = true;\n                        }\n                    }\n                }\n                _ => {}\n            }\n        }\n    }\n\n    pub fn on_delete(&mut self) {\n        match self.current_widget.widget_type {\n            BottomWidgetType::ProcSearch => {\n                let is_in_search_widget = self.is_in_search_widget();\n                if let Some(proc_widget_state) = self\n                    .states\n                    .proc_state\n                    .widget_states\n                    .get_mut(&(self.current_widget.widget_id - 1))\n                {\n                    if is_in_search_widget\n                        && proc_widget_state.proc_search.search_state.is_enabled\n                        && proc_widget_state.cursor_char_index()\n                            < proc_widget_state\n                                .proc_search\n                                .search_state\n                                .current_search_query\n                                .len()\n                    {\n                        let current_cursor = proc_widget_state.cursor_char_index();\n                        proc_widget_state.search_walk_forward();\n\n                        let _ = proc_widget_state\n                            .proc_search\n                            .search_state\n                            .current_search_query\n                            .drain(current_cursor..proc_widget_state.cursor_char_index());\n\n                        proc_widget_state.proc_search.search_state.grapheme_cursor =\n                            GraphemeCursor::new(\n                                current_cursor,\n                                proc_widget_state\n                                    .proc_search\n                                    .search_state\n                                    .current_search_query\n                                    .len(),\n                                true,\n                            );\n\n                        proc_widget_state.update_query();\n                    }\n                }\n            }\n            BottomWidgetType::Proc => {\n                self.kill_current_process();\n            }\n            _ => {}\n        }\n    }\n\n    pub fn on_backspace(&mut self) {\n        if let BottomWidgetType::ProcSearch = self.current_widget.widget_type {\n            let is_in_search_widget = self.is_in_search_widget();\n            if let Some(proc_widget_state) = self\n                .states\n                .proc_state\n                .widget_states\n                .get_mut(&(self.current_widget.widget_id - 1))\n            {\n                if is_in_search_widget\n                    && proc_widget_state.proc_search.search_state.is_enabled\n                    && proc_widget_state.cursor_char_index() > 0\n                {\n                    let current_cursor = proc_widget_state.cursor_char_index();\n                    proc_widget_state.search_walk_back();\n\n                    // Remove the indices in between.\n                    let _ = proc_widget_state\n                        .proc_search\n                        .search_state\n                        .current_search_query\n                        .drain(proc_widget_state.cursor_char_index()..current_cursor);\n\n                    proc_widget_state.proc_search.search_state.grapheme_cursor =\n                        GraphemeCursor::new(\n                            proc_widget_state.cursor_char_index(),\n                            proc_widget_state\n                                .proc_search\n                                .search_state\n                                .current_search_query\n                                .len(),\n                            true,\n                        );\n\n                    proc_widget_state.proc_search.search_state.cursor_direction =\n                        CursorDirection::Left;\n\n                    proc_widget_state.update_query();\n                }\n            }\n        }\n    }\n\n    pub fn on_up_key(&mut self) {\n        if !self.is_in_dialog() {\n            self.decrement_position_count();\n            self.reset_multi_tap_keys();\n        } else if self.help_dialog_state.is_showing_help {\n            self.help_scroll_up();\n            self.reset_multi_tap_keys();\n        } else if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.on_up_key();\n        }\n    }\n\n    pub fn on_down_key(&mut self) {\n        if !self.is_in_dialog() {\n            self.increment_position_count();\n            self.reset_multi_tap_keys();\n        } else if self.help_dialog_state.is_showing_help {\n            self.help_scroll_down();\n            self.reset_multi_tap_keys();\n        } else if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.on_down_key();\n        }\n    }\n\n    pub fn on_left_key(&mut self) {\n        if !self.is_in_dialog() {\n            match self.current_widget.widget_type {\n                BottomWidgetType::Proc => {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.collapse_current_tree_branch_entry();\n                    }\n                }\n                BottomWidgetType::ProcSearch => {\n                    let is_in_search_widget = self.is_in_search_widget();\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id - 1)\n                    {\n                        if is_in_search_widget {\n                            let prev_cursor = proc_widget_state.cursor_char_index();\n                            proc_widget_state.search_walk_back();\n                            if proc_widget_state.cursor_char_index() < prev_cursor {\n                                proc_widget_state.proc_search.search_state.cursor_direction =\n                                    CursorDirection::Left;\n                            }\n                        }\n                    }\n                }\n                BottomWidgetType::Battery => {\n                    #[cfg(feature = \"battery\")]\n                    if self.data_store.get_data().battery_harvest.len() > 1\n                        && let Some(battery_widget_state) = self\n                            .states\n                            .battery_state\n                            .get_mut_widget_state(self.current_widget.widget_id)\n                        && battery_widget_state.currently_selected_battery_index > 0\n                    {\n                        battery_widget_state.currently_selected_battery_index -= 1;\n                    }\n                }\n                _ => {}\n            }\n        } else if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.on_left_key();\n        }\n    }\n\n    pub fn on_right_key(&mut self) {\n        if !self.is_in_dialog() {\n            match self.current_widget.widget_type {\n                BottomWidgetType::Proc => {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.expand_current_tree_branch_entry();\n                    }\n                }\n                BottomWidgetType::ProcSearch => {\n                    let is_in_search_widget = self.is_in_search_widget();\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id - 1)\n                    {\n                        if is_in_search_widget {\n                            let prev_cursor = proc_widget_state.cursor_char_index();\n                            proc_widget_state.search_walk_forward();\n                            if proc_widget_state.cursor_char_index() > prev_cursor {\n                                proc_widget_state.proc_search.search_state.cursor_direction =\n                                    CursorDirection::Right;\n                            }\n                        }\n                    }\n                }\n                BottomWidgetType::Battery => {\n                    #[cfg(feature = \"battery\")]\n                    {\n                        let battery_count = self.data_store.get_data().battery_harvest.len();\n                        if battery_count > 1 {\n                            if let Some(battery_widget_state) = self\n                                .states\n                                .battery_state\n                                .get_mut_widget_state(self.current_widget.widget_id)\n                            {\n                                if battery_widget_state.currently_selected_battery_index\n                                    < battery_count - 1\n                                {\n                                    battery_widget_state.currently_selected_battery_index += 1;\n                                }\n                            }\n                        }\n                    }\n                }\n                _ => {}\n            }\n        } else if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.on_right_key();\n        }\n    }\n\n    pub fn on_space_key(&mut self) {\n        if !self.is_in_dialog() {\n            if self.current_widget.widget_type == BottomWidgetType::Proc {\n                if let Some(proc_widget_state) = self\n                    .states\n                    .proc_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    proc_widget_state.toggle_current_tree_branch_entry();\n                }\n            }\n        } else if self.process_kill_dialog.is_open() {\n            // Either select the current option,\n            // or scroll to the next one\n        }\n    }\n\n    pub fn on_page_up(&mut self) {\n        if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.on_page_up();\n        } else if self.help_dialog_state.is_showing_help {\n            let current = &mut self.help_dialog_state.scroll_state.current_scroll_index;\n            let amount = self.help_dialog_state.height;\n            *current = current.saturating_sub(amount);\n        } else if self.current_widget.widget_type.is_widget_table() {\n            if let (Some((_tlc_x, tlc_y)), Some((_brc_x, brc_y))) = (\n                &self.current_widget.top_left_corner,\n                &self.current_widget.bottom_right_corner,\n            ) {\n                let border_offset = u16::from(self.is_drawing_border());\n                let header_offset = self.header_offset(&self.current_widget);\n                let height = brc_y - tlc_y - 2 * border_offset - header_offset;\n                self.change_position_count(-(height as i64));\n            }\n        }\n    }\n\n    pub fn on_page_down(&mut self) {\n        if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.on_page_down();\n        } else if self.help_dialog_state.is_showing_help {\n            let current = self.help_dialog_state.scroll_state.current_scroll_index;\n            let amount = self.help_dialog_state.height;\n\n            self.help_scroll_to_or_max(current + amount);\n        } else if self.current_widget.widget_type.is_widget_table() {\n            if let (Some((_tlc_x, tlc_y)), Some((_brc_x, brc_y))) = (\n                &self.current_widget.top_left_corner,\n                &self.current_widget.bottom_right_corner,\n            ) {\n                let border_offset = u16::from(self.is_drawing_border());\n                let header_offset = self.header_offset(&self.current_widget);\n                let height = brc_y - tlc_y - 2 * border_offset - header_offset;\n                self.change_position_count(height as i64);\n            }\n        }\n    }\n\n    pub fn scroll_half_page_up(&mut self) {\n        if self.help_dialog_state.is_showing_help {\n            let current = &mut self.help_dialog_state.scroll_state.current_scroll_index;\n            let amount = self.help_dialog_state.height / 2;\n\n            *current = current.saturating_sub(amount);\n        } else if self.current_widget.widget_type.is_widget_table() {\n            if let (Some((_tlc_x, tlc_y)), Some((_brc_x, brc_y))) = (\n                &self.current_widget.top_left_corner,\n                &self.current_widget.bottom_right_corner,\n            ) {\n                let border_offset = u16::from(self.is_drawing_border());\n                let header_offset = self.header_offset(&self.current_widget);\n                let height = brc_y - tlc_y - 2 * border_offset - header_offset;\n                self.change_position_count(-(height as i64) / 2);\n            }\n        }\n    }\n\n    pub fn scroll_half_page_down(&mut self) {\n        if self.help_dialog_state.is_showing_help {\n            let current = self.help_dialog_state.scroll_state.current_scroll_index;\n            let amount = self.help_dialog_state.height / 2;\n\n            self.help_scroll_to_or_max(current + amount);\n        } else if self.current_widget.widget_type.is_widget_table() {\n            if let (Some((_tlc_x, tlc_y)), Some((_brc_x, brc_y))) = (\n                &self.current_widget.top_left_corner,\n                &self.current_widget.bottom_right_corner,\n            ) {\n                let border_offset = u16::from(self.is_drawing_border());\n                let header_offset = self.header_offset(&self.current_widget);\n                let height = brc_y - tlc_y - 2 * border_offset - header_offset;\n                self.change_position_count(height as i64 / 2);\n            }\n        }\n    }\n\n    pub fn skip_cursor_beginning(&mut self) {\n        if !self.ignore_normal_keybinds() {\n            if let BottomWidgetType::ProcSearch = self.current_widget.widget_type {\n                let is_in_search_widget = self.is_in_search_widget();\n                if let Some(proc_widget_state) = self\n                    .states\n                    .proc_state\n                    .widget_states\n                    .get_mut(&(self.current_widget.widget_id - 1))\n                {\n                    if is_in_search_widget {\n                        proc_widget_state.proc_search.search_state.grapheme_cursor =\n                            GraphemeCursor::new(\n                                0,\n                                proc_widget_state\n                                    .proc_search\n                                    .search_state\n                                    .current_search_query\n                                    .len(),\n                                true,\n                            );\n\n                        proc_widget_state.proc_search.search_state.cursor_direction =\n                            CursorDirection::Left;\n                    }\n                }\n            }\n        }\n    }\n\n    pub fn skip_cursor_end(&mut self) {\n        if !self.ignore_normal_keybinds() {\n            if let BottomWidgetType::ProcSearch = self.current_widget.widget_type {\n                let is_in_search_widget = self.is_in_search_widget();\n                if let Some(proc_widget_state) = self\n                    .states\n                    .proc_state\n                    .widget_states\n                    .get_mut(&(self.current_widget.widget_id - 1))\n                {\n                    if is_in_search_widget {\n                        let query_len = proc_widget_state\n                            .proc_search\n                            .search_state\n                            .current_search_query\n                            .len();\n\n                        proc_widget_state.proc_search.search_state.grapheme_cursor =\n                            GraphemeCursor::new(query_len, query_len, true);\n                        proc_widget_state.proc_search.search_state.cursor_direction =\n                            CursorDirection::Right;\n                    }\n                }\n            }\n        }\n    }\n\n    pub fn clear_search(&mut self) {\n        if let BottomWidgetType::ProcSearch = self.current_widget.widget_type {\n            if let Some(proc_widget_state) = self\n                .states\n                .proc_state\n                .widget_states\n                .get_mut(&(self.current_widget.widget_id - 1))\n            {\n                proc_widget_state.clear_search();\n            }\n        }\n    }\n\n    pub fn clear_previous_word(&mut self) {\n        if let BottomWidgetType::ProcSearch = self.current_widget.widget_type {\n            if let Some(proc_widget_state) = self\n                .states\n                .proc_state\n                .widget_states\n                .get_mut(&(self.current_widget.widget_id - 1))\n            {\n                // Traverse backwards from the current cursor location until you hit\n                // non-whitespace characters, then continue to traverse (and\n                // delete) backwards until you hit a whitespace character.  Halt.\n\n                // So... first, let's get our current cursor position in terms of char indices.\n                let end_index = proc_widget_state.cursor_char_index();\n\n                // Then, let's crawl backwards until we hit our location, and store the\n                // \"head\"...\n                let query = proc_widget_state.current_search_query();\n                let mut start_index = 0;\n                let mut saw_non_whitespace = false;\n\n                for (itx, c) in query\n                    .chars()\n                    .rev()\n                    .enumerate()\n                    .skip(query.len() - end_index)\n                {\n                    if c.is_whitespace() {\n                        if saw_non_whitespace {\n                            start_index = query.len() - itx;\n                            break;\n                        }\n                    } else {\n                        saw_non_whitespace = true;\n                    }\n                }\n\n                let _ = proc_widget_state\n                    .proc_search\n                    .search_state\n                    .current_search_query\n                    .drain(start_index..end_index);\n\n                proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new(\n                    start_index,\n                    proc_widget_state\n                        .proc_search\n                        .search_state\n                        .current_search_query\n                        .len(),\n                    true,\n                );\n\n                proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Left;\n\n                proc_widget_state.update_query();\n            }\n        }\n    }\n\n    pub fn on_char_key(&mut self, caught_char: char) {\n        // Skip control code chars\n        if caught_char.is_control() {\n            return;\n        }\n\n        const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u64 = 1000;\n\n        // Forbid any char key presses when showing a dialog box...\n        if !self.ignore_normal_keybinds() {\n            let current_key_press_inst = Instant::now();\n            if current_key_press_inst\n                .duration_since(self.last_key_press)\n                .as_millis()\n                > MAX_KEY_TIMEOUT_IN_MILLISECONDS.into()\n            {\n                self.reset_multi_tap_keys();\n            }\n            self.last_key_press = current_key_press_inst;\n\n            if let BottomWidgetType::ProcSearch = self.current_widget.widget_type {\n                let is_in_search_widget = self.is_in_search_widget();\n                if let Some(proc_widget_state) = self\n                    .states\n                    .proc_state\n                    .widget_states\n                    .get_mut(&(self.current_widget.widget_id - 1))\n                {\n                    if is_in_search_widget && proc_widget_state.is_search_enabled() {\n                        proc_widget_state\n                            .proc_search\n                            .search_state\n                            .current_search_query\n                            .insert(proc_widget_state.cursor_char_index(), caught_char);\n\n                        proc_widget_state.proc_search.search_state.grapheme_cursor =\n                            GraphemeCursor::new(\n                                proc_widget_state.cursor_char_index(),\n                                proc_widget_state\n                                    .proc_search\n                                    .search_state\n                                    .current_search_query\n                                    .len(),\n                                true,\n                            );\n                        proc_widget_state.search_walk_forward();\n\n                        proc_widget_state.update_query();\n                        proc_widget_state.proc_search.search_state.cursor_direction =\n                            CursorDirection::Right;\n\n                        return;\n                    }\n                }\n            }\n            self.handle_char(caught_char);\n        } else if self.help_dialog_state.is_showing_help {\n            match caught_char {\n                '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {\n                    let potential_index = caught_char.to_digit(10);\n                    if let Some(potential_index) = potential_index {\n                        let potential_index = potential_index as usize;\n                        if (potential_index) < self.help_dialog_state.index_shortcuts.len() {\n                            self.help_scroll_to_or_max(\n                                self.help_dialog_state.index_shortcuts[potential_index],\n                            );\n                        }\n                    }\n                }\n                'j' | 'k' | 'g' | 'G' => self.handle_char(caught_char),\n                _ => {}\n            }\n        } else if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.on_char(caught_char);\n        }\n    }\n\n    /// Kill the currently selected process if we are in the process widget.\n    ///\n    /// TODO: This ideally gets abstracted out into a separate widget.\n    pub(crate) fn kill_current_process(&mut self) {\n        if self.app_config_fields.is_read_only {\n            return;\n        }\n\n        if let Some(pws) = self\n            .states\n            .proc_state\n            .widget_states\n            .get(&self.current_widget.widget_id)\n        {\n            if let Some(current) = pws.table.current_item() {\n                let id = current.id.to_string();\n                if let Some(pids) = pws\n                    .id_pid_map\n                    .get(&id)\n                    .cloned()\n                    .or_else(|| Some(vec![current.pid]))\n                {\n                    let current_process = (id, pids);\n\n                    let use_simple_selection = {\n                        cfg_if::cfg_if! {\n                            if #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))] {\n                                !self.app_config_fields.is_advanced_kill\n                            } else {\n                                true\n                            }\n                        }\n                    };\n\n                    self.process_kill_dialog.start_process_kill(\n                        current_process.0,\n                        current_process.1,\n                        use_simple_selection,\n                    );\n\n                    // TODO: I don't think most of this is needed.\n                    self.is_determining_widget_boundary = true;\n                }\n            }\n        }\n    }\n\n    // FIXME: Refactor this system...\n    fn handle_char(&mut self, caught_char: char) {\n        match caught_char {\n            '/' => {\n                self.on_slash();\n            }\n            'd' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    let mut is_first_d = true;\n                    if let Some(second_char) = self.second_char {\n                        if self.awaiting_second_char && second_char == 'd' {\n                            is_first_d = false;\n                            self.awaiting_second_char = false;\n                            self.second_char = None;\n\n                            self.reset_multi_tap_keys();\n\n                            self.kill_current_process();\n                        }\n                    }\n\n                    if is_first_d {\n                        self.awaiting_second_char = true;\n                        self.second_char = Some('d');\n                    }\n                } else if let Some(disk) = self\n                    .states\n                    .disk_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    disk.set_index(0);\n                }\n            }\n            'g' => {\n                let mut is_first_g = true;\n                if let Some(second_char) = self.second_char {\n                    if self.awaiting_second_char && second_char == 'g' {\n                        is_first_g = false;\n                        self.awaiting_second_char = false;\n                        self.second_char = None;\n                        self.skip_to_first();\n                    }\n                }\n\n                if is_first_g {\n                    self.awaiting_second_char = true;\n                    self.second_char = Some('g');\n                }\n            }\n            'G' => self.skip_to_last(),\n            'k' => self.on_up_key(),\n            'j' => self.on_down_key(),\n            'f' => self.data_store.toggle_frozen(),\n            'c' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.select_column(ProcWidgetColumn::Cpu);\n                    }\n                }\n            }\n            'm' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.select_column(ProcWidgetColumn::Mem);\n                    }\n                } else if let Some(disk) = self\n                    .states\n                    .disk_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    disk.set_index(1);\n                }\n            }\n            'p' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.select_column(ProcWidgetColumn::PidOrCount);\n                    }\n                } else if let Some(disk) = self\n                    .states\n                    .disk_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    disk.set_index(5);\n                }\n            }\n            'P' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.toggle_command();\n                    }\n                }\n            }\n            'n' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.select_column(ProcWidgetColumn::ProcNameOrCommand);\n                    }\n                } else if let Some(disk) = self\n                    .states\n                    .disk_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    disk.set_index(3);\n                }\n            }\n            #[cfg(feature = \"gpu\")]\n            'M' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.select_column(ProcWidgetColumn::GpuMem);\n                    }\n                }\n            }\n            #[cfg(feature = \"gpu\")]\n            'C' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.select_column(ProcWidgetColumn::GpuUtil);\n                    }\n                }\n            }\n            '?' => {\n                self.help_dialog_state.is_showing_help = true;\n                self.is_force_redraw = true;\n            }\n            'H' | 'A' => self.move_widget_selection(&WidgetDirection::Left),\n            'L' | 'D' => self.move_widget_selection(&WidgetDirection::Right),\n            'K' | 'W' => self.move_widget_selection(&WidgetDirection::Up),\n            'J' | 'S' => self.move_widget_selection(&WidgetDirection::Down),\n            't' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    self.toggle_tree_mode()\n                } else if let Some(temp) = self\n                    .states\n                    .temp_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    temp.table.set_sort_index(1);\n                    temp.force_data_update();\n                } else if let Some(disk) = self\n                    .states\n                    .disk_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    disk.set_index(4);\n                }\n            }\n            '+' => self.on_plus(),\n            '-' => self.on_minus(),\n            '=' => self.reset_zoom(),\n            'e' => self.toggle_expand_widget(),\n            's' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    self.toggle_sort_menu()\n                } else if let Some(temp) = self\n                    .states\n                    .temp_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    temp.table.set_sort_index(0);\n                    temp.force_data_update();\n                    self.is_force_redraw = true;\n                }\n            }\n            'u' => {\n                if let Some(disk) = self\n                    .states\n                    .disk_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    disk.set_index(2);\n                }\n            }\n            'r' => {\n                if let Some(disk) = self\n                    .states\n                    .disk_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    disk.set_index(6);\n                }\n            }\n            'w' => {\n                if let Some(disk) = self\n                    .states\n                    .disk_state\n                    .get_mut_widget_state(self.current_widget.widget_id)\n                {\n                    disk.set_index(7);\n                }\n            }\n            'I' => self.invert_sort(),\n            '%' => self.toggle_percentages(),\n            #[cfg(target_os = \"linux\")]\n            'z' => {\n                if let BottomWidgetType::Proc = self.current_widget.widget_type {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .widget_states\n                        .get_mut(&self.current_widget.widget_id)\n                    {\n                        proc_widget_state.toggle_k_thread();\n                    }\n                }\n            }\n            _ => {}\n        }\n\n        if let Some(second_char) = self.second_char {\n            if self.awaiting_second_char && caught_char != second_char {\n                self.awaiting_second_char = false;\n            }\n        }\n    }\n\n    fn toggle_expand_widget(&mut self) {\n        if self.is_expanded {\n            self.is_expanded = false;\n            self.is_force_redraw = true;\n        } else {\n            self.expand_widget();\n        }\n    }\n\n    fn expand_widget(&mut self) {\n        // TODO: [BASIC] Expansion in basic mode.\n        if !self.ignore_normal_keybinds() && !self.app_config_fields.use_basic_mode {\n            // Pop-out mode.  We ignore if in process search.\n\n            match self.current_widget.widget_type {\n                BottomWidgetType::ProcSearch => {}\n                _ => {\n                    self.is_expanded = true;\n                    self.is_force_redraw = true;\n                }\n            }\n        }\n    }\n\n    pub fn move_widget_selection(&mut self, direction: &WidgetDirection) {\n        // Since we only want to call reset once, we do it like this to avoid\n        // redundant calls on recursion.\n        self.move_widget_selection_logic(direction);\n        self.reset_multi_tap_keys();\n    }\n\n    fn move_widget_selection_logic(&mut self, direction: &WidgetDirection) {\n        // The actual logic for widget movement.\n\n        // We follow these following steps:\n        // 1. Send a movement signal in `direction`.\n        // 2. Check if this new widget we've landed on is hidden.  If not, halt.\n        // 3. If it hidden, loop and either send:\n        //    - A signal equal to the current direction, if it is opposite of the reflection.\n        //    - Reflection direction.\n\n        if !self.ignore_normal_keybinds() && !self.is_expanded {\n            if let Some(new_widget_id) = &(match direction {\n                WidgetDirection::Left => self.current_widget.left_neighbour,\n                WidgetDirection::Right => self.current_widget.right_neighbour,\n                WidgetDirection::Up => self.current_widget.up_neighbour,\n                WidgetDirection::Down => self.current_widget.down_neighbour,\n            }) {\n                if let Some(new_widget) = self.widget_map.get(new_widget_id) {\n                    match &new_widget.widget_type {\n                        BottomWidgetType::Temp\n                        | BottomWidgetType::Proc\n                        | BottomWidgetType::ProcSort\n                        | BottomWidgetType::Disk\n                        | BottomWidgetType::Battery\n                            if self.states.basic_table_widget_state.is_some()\n                                && (*direction == WidgetDirection::Left\n                                    || *direction == WidgetDirection::Right) =>\n                        {\n                            // Gotta do this for the sort widget\n                            if let BottomWidgetType::ProcSort = new_widget.widget_type {\n                                if let Some(proc_widget_state) = self\n                                    .states\n                                    .proc_state\n                                    .widget_states\n                                    .get(&(new_widget_id - 2))\n                                {\n                                    if proc_widget_state.is_sort_open {\n                                        self.current_widget = new_widget.clone();\n                                    } else if let Some(next_new_widget_id) = match direction {\n                                        WidgetDirection::Left => new_widget.left_neighbour,\n                                        _ => new_widget.right_neighbour,\n                                    } {\n                                        if let Some(next_new_widget) =\n                                            self.widget_map.get(&next_new_widget_id)\n                                        {\n                                            self.current_widget = next_new_widget.clone();\n                                        }\n                                    }\n                                }\n                            } else {\n                                self.current_widget = new_widget.clone();\n                            }\n\n                            if let Some(basic_table_widget_state) =\n                                &mut self.states.basic_table_widget_state\n                            {\n                                basic_table_widget_state.currently_displayed_widget_id =\n                                    self.current_widget.widget_id;\n                                basic_table_widget_state.currently_displayed_widget_type =\n                                    self.current_widget.widget_type.clone();\n                            }\n\n                            // And let's not forget:\n                            self.is_determining_widget_boundary = true;\n                        }\n                        BottomWidgetType::BasicTables => {\n                            match &direction {\n                                WidgetDirection::Up => {\n                                    // Note this case would fail if it moved up into a hidden\n                                    // widget, but it's for basic so whatever, it's all hard-coded\n                                    // right now anyways...\n                                    if let Some(next_new_widget_id) = new_widget.up_neighbour {\n                                        if let Some(next_new_widget) =\n                                            self.widget_map.get(&next_new_widget_id)\n                                        {\n                                            self.current_widget = next_new_widget.clone();\n                                        }\n                                    }\n                                }\n                                WidgetDirection::Down => {\n                                    // Assuming we're in basic mode (BasicTables), then\n                                    // we want to move DOWN to the currently shown widget.\n                                    if let Some(basic_table_widget_state) =\n                                        &mut self.states.basic_table_widget_state\n                                    {\n                                        // We also want to move towards Proc if we had set it to\n                                        // ProcSort.\n                                        if let BottomWidgetType::ProcSort =\n                                            basic_table_widget_state.currently_displayed_widget_type\n                                        {\n                                            basic_table_widget_state\n                                                .currently_displayed_widget_type =\n                                                BottomWidgetType::Proc;\n                                            basic_table_widget_state\n                                                .currently_displayed_widget_id -= 2;\n                                        }\n\n                                        if let Some(next_new_widget) = self.widget_map.get(\n                                            &basic_table_widget_state.currently_displayed_widget_id,\n                                        ) {\n                                            self.current_widget = next_new_widget.clone();\n                                        }\n                                    }\n                                }\n                                _ => self.current_widget = new_widget.clone(),\n                            }\n                        }\n                        _ if new_widget.parent_reflector.is_some() => {\n                            // It may be hidden...\n                            if let Some((parent_direction, offset)) = &new_widget.parent_reflector {\n                                if direction.is_opposite(parent_direction) {\n                                    // Keep going in the current direction if hidden...\n                                    // unless we hit a wall of sorts.\n                                    let option_next_neighbour_id = match &direction {\n                                        WidgetDirection::Left => new_widget.left_neighbour,\n                                        WidgetDirection::Right => new_widget.right_neighbour,\n                                        WidgetDirection::Up => new_widget.up_neighbour,\n                                        WidgetDirection::Down => new_widget.down_neighbour,\n                                    };\n                                    match &new_widget.widget_type {\n                                        BottomWidgetType::CpuLegend => {\n                                            if let Some(cpu_widget_state) = self\n                                                .states\n                                                .cpu_state\n                                                .widget_states\n                                                .get(&(new_widget_id - *offset))\n                                            {\n                                                if cpu_widget_state.is_legend_hidden {\n                                                    if let Some(next_neighbour_id) =\n                                                        option_next_neighbour_id\n                                                    {\n                                                        if let Some(next_neighbour_widget) =\n                                                            self.widget_map.get(&next_neighbour_id)\n                                                        {\n                                                            self.current_widget =\n                                                                next_neighbour_widget.clone();\n                                                        }\n                                                    }\n                                                } else {\n                                                    self.current_widget = new_widget.clone();\n                                                }\n                                            }\n                                        }\n                                        BottomWidgetType::ProcSearch\n                                        | BottomWidgetType::ProcSort => {\n                                            if let Some(proc_widget_state) = self\n                                                .states\n                                                .proc_state\n                                                .widget_states\n                                                .get(&(new_widget_id - *offset))\n                                            {\n                                                match &new_widget.widget_type {\n                                                    BottomWidgetType::ProcSearch =>\n                                                    {\n                                                        #[allow(clippy::collapsible_match)]\n                                                        if !proc_widget_state.is_search_enabled() {\n                                                            if let Some(next_neighbour_id) =\n                                                                option_next_neighbour_id\n                                                                && let Some(next_neighbour_widget) =\n                                                                    self.widget_map\n                                                                        .get(&next_neighbour_id)\n                                                            {\n                                                                self.current_widget =\n                                                                    next_neighbour_widget.clone();\n                                                            }\n                                                        } else {\n                                                            self.current_widget =\n                                                                new_widget.clone();\n                                                        }\n                                                    }\n                                                    BottomWidgetType::ProcSort =>\n                                                    {\n                                                        #[allow(clippy::collapsible_match)]\n                                                        if !proc_widget_state.is_sort_open {\n                                                            if let Some(next_neighbour_id) =\n                                                                option_next_neighbour_id\n                                                                && let Some(next_neighbour_widget) =\n                                                                    self.widget_map\n                                                                        .get(&next_neighbour_id)\n                                                            {\n                                                                self.current_widget =\n                                                                    next_neighbour_widget.clone();\n                                                            }\n                                                        } else {\n                                                            self.current_widget =\n                                                                new_widget.clone();\n                                                        }\n                                                    }\n                                                    _ => {\n                                                        self.current_widget = new_widget.clone();\n                                                    }\n                                                }\n                                            }\n                                        }\n                                        _ => {\n                                            self.current_widget = new_widget.clone();\n                                        }\n                                    }\n                                } else {\n                                    // Reflect\n                                    match &new_widget.widget_type {\n                                        BottomWidgetType::CpuLegend => {\n                                            if let Some(cpu_widget_state) = self\n                                                .states\n                                                .cpu_state\n                                                .widget_states\n                                                .get(&(new_widget_id - *offset))\n                                            {\n                                                if cpu_widget_state.is_legend_hidden {\n                                                    if let Some(parent_cpu_widget) = self\n                                                        .widget_map\n                                                        .get(&(new_widget_id - *offset))\n                                                    {\n                                                        self.current_widget =\n                                                            parent_cpu_widget.clone();\n                                                    }\n                                                } else {\n                                                    self.current_widget = new_widget.clone();\n                                                }\n                                            }\n                                        }\n                                        BottomWidgetType::ProcSearch\n                                        | BottomWidgetType::ProcSort => {\n                                            if let Some(proc_widget_state) = self\n                                                .states\n                                                .proc_state\n                                                .widget_states\n                                                .get(&(new_widget_id - *offset))\n                                            {\n                                                match &new_widget.widget_type {\n                                                    BottomWidgetType::ProcSearch =>\n                                                    {\n                                                        #[allow(clippy::collapsible_match)]\n                                                        if !proc_widget_state.is_search_enabled() {\n                                                            if let Some(parent_proc_widget) = self\n                                                                .widget_map\n                                                                .get(&(new_widget_id - *offset))\n                                                            {\n                                                                self.current_widget =\n                                                                    parent_proc_widget.clone();\n                                                            }\n                                                        } else {\n                                                            self.current_widget =\n                                                                new_widget.clone();\n                                                        }\n                                                    }\n                                                    BottomWidgetType::ProcSort =>\n                                                    {\n                                                        #[allow(clippy::collapsible_match)]\n                                                        if !proc_widget_state.is_sort_open {\n                                                            if let Some(parent_proc_widget) = self\n                                                                .widget_map\n                                                                .get(&(new_widget_id - *offset))\n                                                            {\n                                                                self.current_widget =\n                                                                    parent_proc_widget.clone();\n                                                            }\n                                                        } else {\n                                                            self.current_widget =\n                                                                new_widget.clone();\n                                                        }\n                                                    }\n                                                    _ => {\n                                                        self.current_widget = new_widget.clone();\n                                                    }\n                                                }\n                                            }\n                                        }\n                                        _ => {\n                                            self.current_widget = new_widget.clone();\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        _ => {\n                            // Cannot be hidden, does not special treatment.\n                            self.current_widget = new_widget.clone();\n                        }\n                    }\n\n                    let mut reflection_dir: Option<WidgetDirection> = None;\n                    if let Some((parent_direction, offset)) = &self.current_widget.parent_reflector\n                    {\n                        match &self.current_widget.widget_type {\n                            BottomWidgetType::CpuLegend => {\n                                if let Some(cpu_widget_state) = self\n                                    .states\n                                    .cpu_state\n                                    .widget_states\n                                    .get(&(self.current_widget.widget_id - *offset))\n                                {\n                                    if cpu_widget_state.is_legend_hidden {\n                                        reflection_dir = Some(parent_direction.clone());\n                                    }\n                                }\n                            }\n                            BottomWidgetType::ProcSearch | BottomWidgetType::ProcSort => {\n                                if let Some(proc_widget_state) = self\n                                    .states\n                                    .proc_state\n                                    .widget_states\n                                    .get(&(self.current_widget.widget_id - *offset))\n                                {\n                                    match &self.current_widget.widget_type {\n                                        BottomWidgetType::ProcSearch\n                                            if !proc_widget_state.is_search_enabled() =>\n                                        {\n                                            reflection_dir = Some(parent_direction.clone());\n                                        }\n                                        BottomWidgetType::ProcSort\n                                            if !proc_widget_state.is_sort_open =>\n                                        {\n                                            reflection_dir = Some(parent_direction.clone());\n                                        }\n                                        _ => {}\n                                    }\n                                }\n                            }\n                            _ => {}\n                        }\n                    }\n\n                    if let Some(ref_dir) = &reflection_dir {\n                        self.move_widget_selection_logic(ref_dir);\n                    }\n                }\n            }\n        } else {\n            match direction {\n                WidgetDirection::Left => self.handle_left_expanded_movement(),\n                WidgetDirection::Right => self.handle_right_expanded_movement(),\n                WidgetDirection::Up => {\n                    if let BottomWidgetType::ProcSearch = self.current_widget.widget_type {\n                        if let Some(current_widget) =\n                            self.widget_map.get(&self.current_widget.widget_id)\n                        {\n                            if let Some(new_widget_id) = current_widget.up_neighbour {\n                                if let Some(new_widget) = self.widget_map.get(&new_widget_id) {\n                                    self.current_widget = new_widget.clone();\n                                }\n                            }\n                        }\n                    }\n                }\n                WidgetDirection::Down => match &self.current_widget.widget_type {\n                    BottomWidgetType::Proc | BottomWidgetType::ProcSort => {\n                        let widget_id = self.current_widget.widget_id\n                            - match &self.current_widget.widget_type {\n                                BottomWidgetType::ProcSort => 2,\n                                _ => 0,\n                            };\n                        if let Some(current_widget) = self.widget_map.get(&widget_id) {\n                            if let Some(new_widget_id) = current_widget.down_neighbour {\n                                if let Some(new_widget) = self.widget_map.get(&new_widget_id) {\n                                    if let Some(proc_widget_state) =\n                                        self.states.proc_state.get_widget_state(widget_id)\n                                    {\n                                        if proc_widget_state.is_search_enabled() {\n                                            self.current_widget = new_widget.clone();\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    _ => {}\n                },\n            }\n        }\n    }\n\n    fn handle_left_expanded_movement(&mut self) {\n        if let BottomWidgetType::Proc = self.current_widget.widget_type {\n            if let Some(new_widget_id) = self.current_widget.left_neighbour {\n                if let Some(proc_widget_state) = self\n                    .states\n                    .proc_state\n                    .widget_states\n                    .get(&self.current_widget.widget_id)\n                {\n                    if proc_widget_state.is_sort_open {\n                        if let Some(proc_sort_widget) = self.widget_map.get(&new_widget_id) {\n                            self.current_widget = proc_sort_widget.clone(); // TODO: Could I remove this clone w/ static references?\n                        }\n                    }\n                }\n            }\n        } else if self.app_config_fields.cpu_left_legend {\n            if let BottomWidgetType::Cpu = self.current_widget.widget_type {\n                if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) {\n                    if let Some(cpu_widget_state) = self\n                        .states\n                        .cpu_state\n                        .widget_states\n                        .get(&self.current_widget.widget_id)\n                    {\n                        if !cpu_widget_state.is_legend_hidden {\n                            if let Some(new_widget_id) = current_widget.left_neighbour {\n                                if let Some(new_widget) = self.widget_map.get(&new_widget_id) {\n                                    self.current_widget = new_widget.clone();\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        } else if let BottomWidgetType::CpuLegend = self.current_widget.widget_type {\n            if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) {\n                if let Some(new_widget_id) = current_widget.left_neighbour {\n                    if let Some(new_widget) = self.widget_map.get(&new_widget_id) {\n                        self.current_widget = new_widget.clone();\n                    }\n                }\n            }\n        }\n    }\n\n    fn handle_right_expanded_movement(&mut self) {\n        if let BottomWidgetType::ProcSort = self.current_widget.widget_type {\n            if let Some(new_widget_id) = self.current_widget.right_neighbour {\n                if let Some(proc_sort_widget) = self.widget_map.get(&new_widget_id) {\n                    self.current_widget = proc_sort_widget.clone();\n                }\n            }\n        } else if self.app_config_fields.cpu_left_legend {\n            if let BottomWidgetType::CpuLegend = self.current_widget.widget_type {\n                if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) {\n                    if let Some(new_widget_id) = current_widget.right_neighbour {\n                        if let Some(new_widget) = self.widget_map.get(&new_widget_id) {\n                            self.current_widget = new_widget.clone();\n                        }\n                    }\n                }\n            }\n        } else if let BottomWidgetType::Cpu = self.current_widget.widget_type {\n            if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) {\n                if let Some(cpu_widget_state) = self\n                    .states\n                    .cpu_state\n                    .widget_states\n                    .get(&self.current_widget.widget_id)\n                {\n                    if !cpu_widget_state.is_legend_hidden {\n                        if let Some(new_widget_id) = current_widget.right_neighbour {\n                            if let Some(new_widget) = self.widget_map.get(&new_widget_id) {\n                                self.current_widget = new_widget.clone();\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    pub fn skip_to_first(&mut self) {\n        if !self.ignore_normal_keybinds() {\n            match self.current_widget.widget_type {\n                BottomWidgetType::Proc => {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.table.scroll_to_first();\n                    }\n                }\n                BottomWidgetType::ProcSort => {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id - 2)\n                    {\n                        proc_widget_state.sort_table.scroll_to_first();\n                    }\n                }\n                BottomWidgetType::Temp => {\n                    if let Some(temp_widget_state) = self\n                        .states\n                        .temp_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        temp_widget_state.table.scroll_to_first();\n                    }\n                }\n                BottomWidgetType::Disk => {\n                    if let Some(disk_widget_state) = self\n                        .states\n                        .disk_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        disk_widget_state.table.scroll_to_first();\n                    }\n                }\n                BottomWidgetType::CpuLegend => {\n                    if let Some(cpu_widget_state) = self\n                        .states\n                        .cpu_state\n                        .get_mut_widget_state(self.current_widget.widget_id - 1)\n                    {\n                        cpu_widget_state.table.scroll_to_first();\n                    }\n                }\n\n                _ => {}\n            }\n            self.reset_multi_tap_keys();\n        } else if self.help_dialog_state.is_showing_help {\n            self.help_dialog_state.scroll_state.current_scroll_index = 0;\n        } else if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.go_to_first();\n        }\n    }\n\n    pub fn skip_to_last(&mut self) {\n        if !self.ignore_normal_keybinds() {\n            match self.current_widget.widget_type {\n                BottomWidgetType::Proc => {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        proc_widget_state.table.scroll_to_last();\n                    }\n                }\n                BottomWidgetType::ProcSort => {\n                    if let Some(proc_widget_state) = self\n                        .states\n                        .proc_state\n                        .get_mut_widget_state(self.current_widget.widget_id - 2)\n                    {\n                        proc_widget_state.sort_table.scroll_to_last();\n                    }\n                }\n                BottomWidgetType::Temp => {\n                    if let Some(temp_widget_state) = self\n                        .states\n                        .temp_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        temp_widget_state.table.scroll_to_last();\n                    }\n                }\n                BottomWidgetType::Disk => {\n                    if let Some(disk_widget_state) = self\n                        .states\n                        .disk_state\n                        .get_mut_widget_state(self.current_widget.widget_id)\n                    {\n                        if !self.data_store.get_data().disk_harvest.is_empty() {\n                            disk_widget_state.table.scroll_to_last();\n                        }\n                    }\n                }\n                BottomWidgetType::CpuLegend => {\n                    if let Some(cpu_widget_state) = self\n                        .states\n                        .cpu_state\n                        .get_mut_widget_state(self.current_widget.widget_id - 1)\n                    {\n                        cpu_widget_state.table.scroll_to_last();\n                    }\n                }\n                _ => {}\n            }\n            self.reset_multi_tap_keys();\n        } else if self.help_dialog_state.is_showing_help {\n            self.help_dialog_state.scroll_state.current_scroll_index =\n                self.help_dialog_state.scroll_state.max_scroll_index;\n        } else if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.go_to_last();\n        }\n    }\n\n    pub fn decrement_position_count(&mut self) {\n        self.change_position_count(-1);\n    }\n\n    pub fn increment_position_count(&mut self) {\n        self.change_position_count(1);\n    }\n\n    fn change_position_count(&mut self, amount: i64) {\n        if !self.ignore_normal_keybinds() {\n            match self.current_widget.widget_type {\n                BottomWidgetType::Proc => {\n                    self.change_process_position(amount);\n                }\n                BottomWidgetType::ProcSort => self.change_process_sort_position(amount),\n                BottomWidgetType::Temp => self.change_temp_position(amount),\n                BottomWidgetType::Disk => self.change_disk_position(amount),\n                BottomWidgetType::CpuLegend => self.change_cpu_legend_position(amount),\n                _ => {}\n            }\n        }\n    }\n\n    fn change_process_sort_position(&mut self, num_to_change_by: i64) {\n        if let Some(proc_widget_state) = self\n            .states\n            .proc_state\n            .get_mut_widget_state(self.current_widget.widget_id - 2)\n        {\n            proc_widget_state\n                .sort_table\n                .increment_position(num_to_change_by);\n        }\n    }\n\n    fn change_cpu_legend_position(&mut self, num_to_change_by: i64) {\n        if let Some(cpu_widget_state) = self\n            .states\n            .cpu_state\n            .widget_states\n            .get_mut(&(self.current_widget.widget_id - 1))\n        {\n            cpu_widget_state.table.increment_position(num_to_change_by);\n        }\n    }\n\n    /// Returns the new position.\n    fn change_process_position(&mut self, num_to_change_by: i64) -> Option<usize> {\n        if let Some(proc_widget_state) = self\n            .states\n            .proc_state\n            .get_mut_widget_state(self.current_widget.widget_id)\n        {\n            proc_widget_state.table.increment_position(num_to_change_by)\n        } else {\n            None\n        }\n    }\n\n    fn change_temp_position(&mut self, num_to_change_by: i64) {\n        if let Some(temp_widget_state) = self\n            .states\n            .temp_state\n            .widget_states\n            .get_mut(&self.current_widget.widget_id)\n        {\n            temp_widget_state.table.increment_position(num_to_change_by);\n        }\n    }\n\n    fn change_disk_position(&mut self, num_to_change_by: i64) {\n        if let Some(disk_widget_state) = self\n            .states\n            .disk_state\n            .widget_states\n            .get_mut(&self.current_widget.widget_id)\n        {\n            disk_widget_state.table.increment_position(num_to_change_by);\n        }\n    }\n\n    fn help_scroll_up(&mut self) {\n        if self.help_dialog_state.scroll_state.current_scroll_index > 0 {\n            self.help_dialog_state.scroll_state.current_scroll_index -= 1;\n        }\n    }\n\n    fn help_scroll_down(&mut self) {\n        if self.help_dialog_state.scroll_state.current_scroll_index\n            < self.help_dialog_state.scroll_state.max_scroll_index\n        {\n            self.help_dialog_state.scroll_state.current_scroll_index += 1;\n        }\n    }\n\n    fn help_scroll_to_or_max(&mut self, new_position: u16) {\n        if new_position <= self.help_dialog_state.scroll_state.max_scroll_index {\n            self.help_dialog_state.scroll_state.current_scroll_index = new_position;\n        } else {\n            self.help_dialog_state.scroll_state.current_scroll_index =\n                self.help_dialog_state.scroll_state.max_scroll_index;\n        }\n    }\n\n    pub fn handle_scroll_up(&mut self) {\n        if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.on_scroll_up();\n        } else if self.help_dialog_state.is_showing_help {\n            self.help_scroll_up();\n        } else if self.current_widget.widget_type.is_widget_graph() {\n            self.zoom_in();\n        } else if self.current_widget.widget_type.is_widget_table() {\n            self.decrement_position_count();\n        }\n    }\n\n    pub fn handle_scroll_down(&mut self) {\n        if self.process_kill_dialog.is_open() {\n            self.process_kill_dialog.on_scroll_down();\n        } else if self.help_dialog_state.is_showing_help {\n            self.help_scroll_down();\n        } else if self.current_widget.widget_type.is_widget_graph() {\n            self.zoom_out();\n        } else if self.current_widget.widget_type.is_widget_table() {\n            self.increment_position_count();\n        }\n    }\n\n    fn on_plus(&mut self) {\n        if let BottomWidgetType::Proc = self.current_widget.widget_type {\n            // Toggle collapsing if tree\n            self.toggle_collapsing_process_branch();\n        } else {\n            self.zoom_in();\n        }\n    }\n\n    fn on_minus(&mut self) {\n        if let BottomWidgetType::Proc = self.current_widget.widget_type {\n            // Toggle collapsing if tree\n            self.toggle_collapsing_process_branch();\n        } else {\n            self.zoom_out();\n        }\n    }\n\n    fn toggle_collapsing_process_branch(&mut self) {\n        if let Some(pws) = self\n            .states\n            .proc_state\n            .widget_states\n            .get_mut(&self.current_widget.widget_id)\n        {\n            pws.toggle_current_tree_branch_entry();\n        }\n    }\n\n    fn zoom_out(&mut self) {\n        match self.current_widget.widget_type {\n            BottomWidgetType::Cpu => {\n                if let Some(cpu_widget_state) = self\n                    .states\n                    .cpu_state\n                    .widget_states\n                    .get_mut(&self.current_widget.widget_id)\n                {\n                    let new_time = cpu_widget_state\n                        .current_display_time\n                        .saturating_add(self.app_config_fields.time_interval);\n\n                    if new_time <= self.app_config_fields.retention_ms {\n                        cpu_widget_state.current_display_time = new_time;\n                        if self.app_config_fields.autohide_time {\n                            cpu_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    } else if cpu_widget_state.current_display_time\n                        != self.app_config_fields.retention_ms\n                    {\n                        cpu_widget_state.current_display_time = self.app_config_fields.retention_ms;\n                        if self.app_config_fields.autohide_time {\n                            cpu_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    }\n                }\n            }\n            BottomWidgetType::Mem => {\n                if let Some(mem_widget_state) = self\n                    .states\n                    .mem_state\n                    .widget_states\n                    .get_mut(&self.current_widget.widget_id)\n                {\n                    let new_time = mem_widget_state\n                        .current_display_time\n                        .saturating_add(self.app_config_fields.time_interval);\n\n                    if new_time <= self.app_config_fields.retention_ms {\n                        mem_widget_state.current_display_time = new_time;\n                        if self.app_config_fields.autohide_time {\n                            mem_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    } else if mem_widget_state.current_display_time\n                        != self.app_config_fields.retention_ms\n                    {\n                        mem_widget_state.current_display_time = self.app_config_fields.retention_ms;\n                        if self.app_config_fields.autohide_time {\n                            mem_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    }\n                }\n            }\n            BottomWidgetType::Net => {\n                if let Some(net_widget_state) = self\n                    .states\n                    .net_state\n                    .widget_states\n                    .get_mut(&self.current_widget.widget_id)\n                {\n                    let new_time = net_widget_state\n                        .current_display_time\n                        .saturating_add(self.app_config_fields.time_interval);\n\n                    if new_time <= self.app_config_fields.retention_ms {\n                        net_widget_state.current_display_time = new_time;\n                        if self.app_config_fields.autohide_time {\n                            net_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    } else if net_widget_state.current_display_time\n                        != self.app_config_fields.retention_ms\n                    {\n                        net_widget_state.current_display_time = self.app_config_fields.retention_ms;\n                        if self.app_config_fields.autohide_time {\n                            net_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    }\n                }\n            }\n            _ => {}\n        }\n    }\n\n    fn zoom_in(&mut self) {\n        match self.current_widget.widget_type {\n            BottomWidgetType::Cpu => {\n                if let Some(cpu_widget_state) = self\n                    .states\n                    .cpu_state\n                    .widget_states\n                    .get_mut(&self.current_widget.widget_id)\n                {\n                    let new_time = cpu_widget_state\n                        .current_display_time\n                        .saturating_sub(self.app_config_fields.time_interval);\n\n                    if new_time >= STALE_MIN_MILLISECONDS {\n                        cpu_widget_state.current_display_time = new_time;\n                        if self.app_config_fields.autohide_time {\n                            cpu_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    } else if cpu_widget_state.current_display_time != STALE_MIN_MILLISECONDS {\n                        cpu_widget_state.current_display_time = STALE_MIN_MILLISECONDS;\n                        if self.app_config_fields.autohide_time {\n                            cpu_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    }\n                }\n            }\n            BottomWidgetType::Mem => {\n                if let Some(mem_widget_state) = self\n                    .states\n                    .mem_state\n                    .widget_states\n                    .get_mut(&self.current_widget.widget_id)\n                {\n                    let new_time = mem_widget_state\n                        .current_display_time\n                        .saturating_sub(self.app_config_fields.time_interval);\n\n                    if new_time >= STALE_MIN_MILLISECONDS {\n                        mem_widget_state.current_display_time = new_time;\n                        if self.app_config_fields.autohide_time {\n                            mem_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    } else if mem_widget_state.current_display_time != STALE_MIN_MILLISECONDS {\n                        mem_widget_state.current_display_time = STALE_MIN_MILLISECONDS;\n                        if self.app_config_fields.autohide_time {\n                            mem_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    }\n                }\n            }\n            BottomWidgetType::Net => {\n                if let Some(net_widget_state) = self\n                    .states\n                    .net_state\n                    .widget_states\n                    .get_mut(&self.current_widget.widget_id)\n                {\n                    let new_time = net_widget_state\n                        .current_display_time\n                        .saturating_sub(self.app_config_fields.time_interval);\n\n                    if new_time >= STALE_MIN_MILLISECONDS {\n                        net_widget_state.current_display_time = new_time;\n                        if self.app_config_fields.autohide_time {\n                            net_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    } else if net_widget_state.current_display_time != STALE_MIN_MILLISECONDS {\n                        net_widget_state.current_display_time = STALE_MIN_MILLISECONDS;\n                        if self.app_config_fields.autohide_time {\n                            net_widget_state.autohide_timer = Some(Instant::now());\n                        }\n                    }\n                }\n            }\n            _ => {}\n        }\n    }\n\n    fn reset_cpu_zoom(&mut self) {\n        if let Some(cpu_widget_state) = self\n            .states\n            .cpu_state\n            .widget_states\n            .get_mut(&self.current_widget.widget_id)\n        {\n            cpu_widget_state.current_display_time = self.app_config_fields.default_time_value;\n            if self.app_config_fields.autohide_time {\n                cpu_widget_state.autohide_timer = Some(Instant::now());\n            }\n        }\n    }\n\n    fn reset_mem_zoom(&mut self) {\n        if let Some(mem_widget_state) = self\n            .states\n            .mem_state\n            .widget_states\n            .get_mut(&self.current_widget.widget_id)\n        {\n            mem_widget_state.current_display_time = self.app_config_fields.default_time_value;\n            if self.app_config_fields.autohide_time {\n                mem_widget_state.autohide_timer = Some(Instant::now());\n            }\n        }\n    }\n\n    fn reset_net_zoom(&mut self) {\n        if let Some(net_widget_state) = self\n            .states\n            .net_state\n            .widget_states\n            .get_mut(&self.current_widget.widget_id)\n        {\n            net_widget_state.current_display_time = self.app_config_fields.default_time_value;\n            if self.app_config_fields.autohide_time {\n                net_widget_state.autohide_timer = Some(Instant::now());\n            }\n        }\n    }\n\n    fn reset_zoom(&mut self) {\n        match self.current_widget.widget_type {\n            BottomWidgetType::Cpu => self.reset_cpu_zoom(),\n            BottomWidgetType::Mem => self.reset_mem_zoom(),\n            BottomWidgetType::Net => self.reset_net_zoom(),\n            _ => {}\n        }\n    }\n\n    /// Moves the mouse to the widget that was clicked on, then propagates the\n    /// click down to be handled by the widget specifically.\n    pub fn on_left_mouse_up(&mut self, x: u16, y: u16) {\n        // Pretty dead simple - iterate through the widget map and go to the widget\n        // where the click is within.\n\n        // TODO: [REFACTOR] might want to refactor this, it's really ugly.\n        // TODO: [REFACTOR] Might wanna refactor ALL state things in general, currently\n        // everything is grouped up as an app state.  We should separate stuff\n        // like event state and gui state and etc.\n\n        // TODO: [MOUSE] double click functionality...?  We would do this above all\n        // other actions and SC if needed.\n\n        // Short circuit if we're in basic table... we might have to handle the basic\n        // table arrow case here...\n\n        if let Some(bt) = &mut self.states.basic_table_widget_state {\n            if let (\n                Some((left_tlc_x, left_tlc_y)),\n                Some((left_brc_x, left_brc_y)),\n                Some((right_tlc_x, right_tlc_y)),\n                Some((right_brc_x, right_brc_y)),\n            ) = (bt.left_tlc, bt.left_brc, bt.right_tlc, bt.right_brc)\n            {\n                if (x >= left_tlc_x && y >= left_tlc_y) && (x < left_brc_x && y < left_brc_y) {\n                    // Case for the left \"button\" in the simple arrow.\n                    if let Some(new_widget) =\n                        self.widget_map.get(&(bt.currently_displayed_widget_id))\n                    {\n                        // We have to move to the current table widget first...\n                        self.current_widget = new_widget.clone();\n\n                        if let BottomWidgetType::Proc = &new_widget.widget_type {\n                            if let Some(proc_widget_state) = self\n                                .states\n                                .proc_state\n                                .get_widget_state(new_widget.widget_id)\n                            {\n                                if proc_widget_state.is_sort_open {\n                                    self.move_widget_selection(&WidgetDirection::Left);\n                                }\n                            }\n                        }\n                        self.move_widget_selection(&WidgetDirection::Left);\n                        return;\n                    }\n                } else if (x >= right_tlc_x && y >= right_tlc_y)\n                    && (x < right_brc_x && y < right_brc_y)\n                {\n                    // Case for the right \"button\" in the simple arrow.\n                    if let Some(new_widget) =\n                        self.widget_map.get(&(bt.currently_displayed_widget_id))\n                    {\n                        // We have to move to the current table widget first...\n                        self.current_widget = new_widget.clone();\n\n                        if let BottomWidgetType::ProcSort = &new_widget.widget_type {\n                            if let Some(proc_widget_state) = self\n                                .states\n                                .proc_state\n                                .get_widget_state(new_widget.widget_id - 2)\n                            {\n                                if proc_widget_state.is_sort_open {\n                                    self.move_widget_selection(&WidgetDirection::Right);\n                                }\n                            }\n                        }\n                    }\n                    self.move_widget_selection(&WidgetDirection::Right);\n                    // Bit extra logic to ensure you always land on a proc widget, not the sort\n                    if let BottomWidgetType::ProcSort = &self.current_widget.widget_type {\n                        self.move_widget_selection(&WidgetDirection::Right);\n                    }\n                    return;\n                }\n            }\n        }\n\n        // Second short circuit --- are we in the dd dialog state?  If so, only check\n        // yes/no/signals and bail after.\n        if self.process_kill_dialog.is_open() && self.process_kill_dialog.on_click(x, y) {\n            return;\n        }\n\n        let mut failed_to_get = true;\n        for (new_widget_id, widget) in &self.widget_map {\n            if let (Some((tlc_x, tlc_y)), Some((brc_x, brc_y))) =\n                (widget.top_left_corner, widget.bottom_right_corner)\n            {\n                if (x >= tlc_x && y >= tlc_y) && (x < brc_x && y < brc_y) {\n                    if let Some(new_widget) = self.widget_map.get(new_widget_id) {\n                        self.current_widget = new_widget.clone();\n                        match &self.current_widget.widget_type {\n                            BottomWidgetType::Temp\n                            | BottomWidgetType::Proc\n                            | BottomWidgetType::ProcSort\n                            | BottomWidgetType::Disk\n                            | BottomWidgetType::Battery => {\n                                if let Some(basic_table_widget_state) =\n                                    &mut self.states.basic_table_widget_state\n                                {\n                                    basic_table_widget_state.currently_displayed_widget_id =\n                                        self.current_widget.widget_id;\n                                    basic_table_widget_state.currently_displayed_widget_type =\n                                        self.current_widget.widget_type.clone();\n                                }\n                            }\n                            _ => {}\n                        }\n\n                        failed_to_get = false;\n                        break;\n                    }\n                }\n            }\n        }\n\n        if failed_to_get {\n            return;\n        }\n\n        // Now handle click propagation down to widget.\n        if let (Some((_tlc_x, tlc_y)), Some((_brc_x, brc_y))) = (\n            &self.current_widget.top_left_corner,\n            &self.current_widget.bottom_right_corner,\n        ) {\n            let border_offset = u16::from(self.is_drawing_border());\n\n            // This check ensures the click isn't actually just clicking on the bottom\n            // border.\n            if y < (brc_y - border_offset) {\n                match &self.current_widget.widget_type {\n                    BottomWidgetType::Proc\n                    | BottomWidgetType::ProcSort\n                    | BottomWidgetType::CpuLegend\n                    | BottomWidgetType::Temp\n                    | BottomWidgetType::Disk => {\n                        // Get our index...\n                        let clicked_entry = y - *tlc_y;\n                        let header_offset = self.header_offset(&self.current_widget);\n                        let offset = border_offset + header_offset;\n                        if clicked_entry >= offset {\n                            let offset_clicked_entry = clicked_entry - offset;\n                            match &self.current_widget.widget_type {\n                                BottomWidgetType::Proc => {\n                                    if let Some(proc_widget_state) = self\n                                        .states\n                                        .proc_state\n                                        .get_widget_state(self.current_widget.widget_id)\n                                    {\n                                        if let Some(visual_index) =\n                                            proc_widget_state.table.ratatui_selected()\n                                        {\n                                            let is_tree_mode = matches!(\n                                                proc_widget_state.mode,\n                                                ProcWidgetMode::Tree { .. }\n                                            );\n                                            let change =\n                                                offset_clicked_entry as i64 - visual_index as i64;\n\n                                            self.change_process_position(change);\n\n                                            // If in tree mode, also check to see if this click is\n                                            // on\n                                            // the same entry as the already selected one - if it\n                                            // is,\n                                            // then we minimize.\n                                            if is_tree_mode && change == 0 {\n                                                self.toggle_collapsing_process_branch();\n                                            }\n                                        }\n                                    }\n                                }\n                                BottomWidgetType::ProcSort => {\n                                    // TODO: [Feature] This could sort if you double click!\n                                    if let Some(proc_widget_state) = self\n                                        .states\n                                        .proc_state\n                                        .get_widget_state(self.current_widget.widget_id - 2)\n                                    {\n                                        if let Some(visual_index) =\n                                            proc_widget_state.sort_table.ratatui_selected()\n                                        {\n                                            self.change_process_sort_position(\n                                                offset_clicked_entry as i64 - visual_index as i64,\n                                            );\n                                        }\n                                    }\n                                }\n                                BottomWidgetType::CpuLegend => {\n                                    if let Some(cpu_widget_state) = self\n                                        .states\n                                        .cpu_state\n                                        .get_widget_state(self.current_widget.widget_id - 1)\n                                    {\n                                        if let Some(visual_index) =\n                                            cpu_widget_state.table.ratatui_selected()\n                                        {\n                                            self.change_cpu_legend_position(\n                                                offset_clicked_entry as i64 - visual_index as i64,\n                                            );\n                                        }\n                                    }\n                                }\n                                BottomWidgetType::Temp => {\n                                    if let Some(temp_widget_state) = self\n                                        .states\n                                        .temp_state\n                                        .get_widget_state(self.current_widget.widget_id)\n                                    {\n                                        if let Some(visual_index) =\n                                            temp_widget_state.table.ratatui_selected()\n                                        {\n                                            self.change_temp_position(\n                                                offset_clicked_entry as i64 - visual_index as i64,\n                                            );\n                                        }\n                                    }\n                                }\n                                BottomWidgetType::Disk => {\n                                    if let Some(disk_widget_state) = self\n                                        .states\n                                        .disk_state\n                                        .get_widget_state(self.current_widget.widget_id)\n                                    {\n                                        if let Some(visual_index) =\n                                            disk_widget_state.table.ratatui_selected()\n                                        {\n                                            self.change_disk_position(\n                                                offset_clicked_entry as i64 - visual_index as i64,\n                                            );\n                                        }\n                                    }\n                                }\n                                _ => {}\n                            }\n                        } else {\n                            // We might have clicked on a header!  Check if we only exceeded the\n                            // table + border offset, and it's implied\n                            // we exceeded the gap offset.\n                            if clicked_entry == border_offset {\n                                match &self.current_widget.widget_type {\n                                    BottomWidgetType::Proc => {\n                                        if let Some(state) = self\n                                            .states\n                                            .proc_state\n                                            .get_mut_widget_state(self.current_widget.widget_id)\n                                        {\n                                            if state.table.try_select_location(x, y).is_some() {\n                                                state.force_data_update();\n                                            }\n                                        }\n                                    }\n                                    BottomWidgetType::Temp => {\n                                        if let Some(temp) = self\n                                            .states\n                                            .temp_state\n                                            .get_mut_widget_state(self.current_widget.widget_id)\n                                        {\n                                            if temp.table.try_select_location(x, y).is_some() {\n                                                temp.force_data_update();\n                                            }\n                                        }\n                                    }\n                                    BottomWidgetType::Disk => {\n                                        if let Some(disk) = self\n                                            .states\n                                            .disk_state\n                                            .get_mut_widget_state(self.current_widget.widget_id)\n                                        {\n                                            if disk.table.try_select_location(x, y).is_some() {\n                                                disk.force_data_update();\n                                            }\n                                        }\n                                    }\n                                    _ => (),\n                                }\n                            }\n                        }\n                    }\n                    BottomWidgetType::Battery => {\n                        #[cfg(feature = \"battery\")]\n                        if let Some(battery_widget_state) = self\n                            .states\n                            .battery_state\n                            .get_mut_widget_state(self.current_widget.widget_id)\n                        {\n                            if let Some(tab_spacing) = &battery_widget_state.tab_click_locs {\n                                for (itx, ((tlc_x, tlc_y), (brc_x, brc_y))) in\n                                    tab_spacing.iter().enumerate()\n                                {\n                                    if (x >= *tlc_x && y >= *tlc_y) && (x <= *brc_x && y <= *brc_y)\n                                    {\n                                        let num_batteries =\n                                            self.data_store.get_data().battery_harvest.len();\n                                        if itx >= num_batteries {\n                                            // range check to keep within current data\n                                            battery_widget_state.currently_selected_battery_index =\n                                                num_batteries - 1;\n                                        } else {\n                                            battery_widget_state.currently_selected_battery_index =\n                                                itx;\n                                        }\n                                        break;\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    _ => {}\n                }\n            }\n        }\n    }\n\n    fn is_drawing_border(&self) -> bool {\n        self.is_expanded || !self.app_config_fields.use_basic_mode\n    }\n\n    fn header_offset(&self, widget: &BottomWidget) -> u16 {\n        if let (Some((_tlc_x, tlc_y)), Some((_brc_x, brc_y))) =\n            (widget.top_left_corner, widget.bottom_right_corner)\n        {\n            let height_diff = brc_y - tlc_y;\n            if height_diff >= constants::TABLE_GAP_HEIGHT_LIMIT {\n                1 + self.app_config_fields.table_gap\n            } else {\n                let min_height_for_header = if self.is_drawing_border() { 3 } else { 1 };\n                u16::from(height_diff > min_height_for_header)\n            }\n        } else {\n            1 + self.app_config_fields.table_gap\n        }\n    }\n\n    /// A quick and dirty way to handle paste events.\n    pub fn handle_paste(&mut self, paste: String) {\n        // Partially copy-pasted from the single-char variant; should probably clean up\n        // this process in the future. In particular, encapsulate this entire\n        // logic and add some tests to make it less potentially error-prone.\n        let is_in_search_widget = self.is_in_search_widget();\n        if let Some(proc_widget_state) = self\n            .states\n            .proc_state\n            .widget_states\n            .get_mut(&(self.current_widget.widget_id - 1))\n        {\n            let num_runes = UnicodeSegmentation::graphemes(paste.as_str(), true).count();\n\n            if is_in_search_widget && proc_widget_state.is_search_enabled() {\n                let left_bound = proc_widget_state.cursor_char_index();\n\n                let curr_query = &mut proc_widget_state\n                    .proc_search\n                    .search_state\n                    .current_search_query;\n                let (left, right) = curr_query.split_at(left_bound);\n                *curr_query = concat_string!(left, paste, right);\n\n                proc_widget_state.proc_search.search_state.grapheme_cursor =\n                    GraphemeCursor::new(left_bound, curr_query.len(), true);\n\n                for _ in 0..num_runes {\n                    proc_widget_state.search_walk_forward();\n                }\n\n                proc_widget_state.update_query();\n                proc_widget_state.proc_search.search_state.cursor_direction =\n                    CursorDirection::Right;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/bin/main.rs",
    "content": "//! Main entrypoint for the application.\n\nuse bottom::{reset_stdout, start_bottom};\n\nfn main() -> anyhow::Result<()> {\n    let mut run_error_hook = false;\n\n    start_bottom(&mut run_error_hook).inspect_err(|_| {\n        if run_error_hook {\n            reset_stdout();\n        }\n    })\n}\n"
  },
  {
    "path": "src/bin/schema.rs",
    "content": "#![cfg(feature = \"generate_schema\")]\n#![expect(\n    clippy::unwrap_used,\n    reason = \"this is just used to generate jsonschema files\"\n)]\n\nuse bottom::{options::config, widgets};\nuse clap::Parser;\nuse itertools::Itertools;\nuse serde_json::Value;\nuse strum::VariantArray;\n\n#[derive(Parser)]\nstruct SchemaOptions {\n    /// The version of the schema.\n    version: Option<String>,\n}\n\nfn generate_schema(schema_options: SchemaOptions) -> anyhow::Result<()> {\n    let mut schema = schemars::schema_for!(config::Config);\n    {\n        // TODO: Maybe make this case insensitive? See https://stackoverflow.com/a/68639341\n\n        match schema\n            .as_object_mut()\n            .unwrap()\n            .get_mut(\"$defs\")\n            .unwrap()\n            .get_mut(\"ProcColumn\")\n            .unwrap()\n        {\n            Value::Object(proc_columns) => {\n                let enums = proc_columns.get_mut(\"enum\").unwrap();\n                *enums = widgets::ProcColumn::VARIANTS\n                    .iter()\n                    .flat_map(|var| var.get_schema_names())\n                    .sorted()\n                    .map(|v| serde_json::Value::String(v.to_string()))\n                    .dedup()\n                    .collect();\n            }\n            _ => anyhow::bail!(\"missing proc columns definition\"),\n        }\n\n        match schema\n            .as_object_mut()\n            .unwrap()\n            .get_mut(\"$defs\")\n            .unwrap()\n            .get_mut(\"DiskColumn\")\n            .unwrap()\n        {\n            Value::Object(disk_columns) => {\n                let enums = disk_columns.get_mut(\"enum\").unwrap();\n                *enums = widgets::DiskColumn::VARIANTS\n                    .iter()\n                    .flat_map(|var| var.get_schema_names())\n                    .sorted()\n                    .map(|v| serde_json::Value::String(v.to_string()))\n                    .dedup()\n                    .collect();\n            }\n            _ => anyhow::bail!(\"missing disk columns definition\"),\n        }\n    }\n\n    let version = schema_options.version.unwrap_or(\"nightly\".to_string());\n    schema.insert(\n        \"$id\".into(),\n        format!(\"https://github.com/ClementTsang/bottom/blob/main/schema/{version}/bottom.json\")\n            .into(),\n    );\n\n    schema.insert(\n        \"description\".into(),\n        format!(\n            \"https://bottom.pages.dev/{}/configuration/config-file/\",\n            if version == \"nightly\" {\n                \"nightly\"\n            } else {\n                version.as_str()\n            }\n        )\n        .into(),\n    );\n\n    let description_version = if version == \"nightly\" {\n        \"nightly\".to_string()\n    } else {\n        format!(\"v{version}\")\n    };\n    schema.insert(\n        \"title\".into(),\n        format!(\"Schema for bottom's config file ({description_version})\").into(),\n    );\n\n    println!(\"{}\", serde_json::to_string_pretty(&schema).unwrap());\n\n    Ok(())\n}\n\nfn main() -> anyhow::Result<()> {\n    let schema_options = SchemaOptions::parse();\n    generate_schema(schema_options)?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/canvas/components/data_table/column.rs",
    "content": "use std::{\n    borrow::Cow,\n    cmp::{max, min},\n    num::NonZeroU16,\n};\n\n/// A bound on the width of a column.\n#[derive(Clone, Copy, Debug)]\npub enum ColumnWidthBounds {\n    /// A width of this type is as long as `desired`, but can otherwise shrink\n    /// and grow up to a point.\n    Soft {\n        /// The desired, calculated width. Take this if possible as the base\n        /// starting width.\n        desired: u16,\n\n        /// The max width, as a percentage of the total width available. If\n        /// [`None`], then it can grow as desired.\n        max_percentage: Option<f32>,\n    },\n\n    /// A width of this type is either as long as specified, or does not appear\n    /// at all.\n    Hard(u16),\n\n    /// A width of this type always resizes to the column header's text width.\n    FollowHeader,\n}\n\npub trait ColumnHeader {\n    /// The \"text\" version of the column header.\n    fn text(&self) -> Cow<'static, str>;\n\n    /// The version displayed when drawing the table. Defaults to\n    /// [`ColumnHeader::text`].\n    #[inline(always)]\n    fn header(&self) -> Cow<'static, str> {\n        self.text()\n    }\n}\n\nimpl ColumnHeader for &'static str {\n    fn text(&self) -> Cow<'static, str> {\n        Cow::Borrowed(self)\n    }\n}\n\nimpl ColumnHeader for String {\n    fn text(&self) -> Cow<'static, str> {\n        Cow::Owned(self.clone())\n    }\n}\n\npub trait DataTableColumn<H: ColumnHeader> {\n    fn inner(&self) -> &H;\n\n    fn inner_mut(&mut self) -> &mut H;\n\n    fn bounds(&self) -> ColumnWidthBounds;\n\n    fn bounds_mut(&mut self) -> &mut ColumnWidthBounds;\n\n    fn is_hidden(&self) -> bool;\n\n    /// The actually displayed \"header\".\n    fn header(&self) -> Cow<'static, str>;\n\n    /// The header length, along with any required additional lengths for things\n    /// like arrows. Defaults to getting the length of\n    /// [`DataTableColumn::header`].\n    fn header_len(&self) -> usize {\n        self.header().len()\n    }\n}\n\n#[derive(Clone, Debug)]\npub struct Column<H> {\n    /// The inner column header.\n    inner: H,\n\n    /// A restriction on this column's width.\n    bounds: ColumnWidthBounds,\n\n    /// Marks that this column is currently \"hidden\", and should *always* be\n    /// skipped.\n    is_hidden: bool,\n}\n\nimpl<H: ColumnHeader> DataTableColumn<H> for Column<H> {\n    #[inline]\n    fn inner(&self) -> &H {\n        &self.inner\n    }\n\n    #[inline]\n    fn inner_mut(&mut self) -> &mut H {\n        &mut self.inner\n    }\n\n    #[inline]\n    fn bounds(&self) -> ColumnWidthBounds {\n        self.bounds\n    }\n\n    #[inline]\n    fn bounds_mut(&mut self) -> &mut ColumnWidthBounds {\n        &mut self.bounds\n    }\n\n    #[inline]\n    fn is_hidden(&self) -> bool {\n        self.is_hidden\n    }\n\n    fn header(&self) -> Cow<'static, str> {\n        self.inner.text()\n    }\n}\n\nimpl<H: ColumnHeader> Column<H> {\n    pub const fn hard(inner: H, width: u16) -> Self {\n        Self {\n            inner,\n            bounds: ColumnWidthBounds::Hard(width),\n            is_hidden: false,\n        }\n    }\n\n    pub const fn soft(inner: H, max_percentage: Option<f32>) -> Self {\n        Self {\n            inner,\n            bounds: ColumnWidthBounds::Soft {\n                desired: 0,\n                max_percentage,\n            },\n            is_hidden: false,\n        }\n    }\n}\n\npub trait CalculateColumnWidths<H> {\n    /// Calculates widths for the columns of this table, given the current width\n    /// when called.\n    ///\n    /// * `total_width` is the total width on the canvas that the columns can\n    ///   try and work with.\n    /// * `left_to_right` is whether to size from left-to-right (`true`) or\n    ///   right-to-left (`false`).\n    fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<NonZeroU16>;\n}\n\nimpl<H, C> CalculateColumnWidths<H> for [C]\nwhere\n    H: ColumnHeader,\n    C: DataTableColumn<H>,\n{\n    fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<NonZeroU16> {\n        use itertools::Either;\n\n        const COLUMN_SPACING: u16 = 1;\n\n        #[inline]\n        fn stop_allocating_space(desired: u16, available: u16) -> bool {\n            desired > available || desired == 0\n        }\n\n        let mut total_width_left = total_width;\n        let mut calculated_widths = vec![];\n        let columns = if left_to_right {\n            Either::Left(self.iter())\n        } else {\n            Either::Right(self.iter().rev())\n        };\n\n        for column in columns {\n            if column.is_hidden() {\n                continue;\n            }\n\n            match &column.bounds() {\n                ColumnWidthBounds::Soft {\n                    desired,\n                    max_percentage,\n                } => {\n                    let min_width = column.header_len() as u16;\n                    if min_width > total_width_left {\n                        break;\n                    }\n\n                    let soft_limit = max(\n                        if let Some(max_percentage) = max_percentage {\n                            ((*max_percentage * f32::from(total_width)).ceil()) as u16\n                        } else {\n                            *desired\n                        },\n                        min_width,\n                    );\n                    let space_taken = min(min(soft_limit, *desired), total_width_left);\n\n                    if stop_allocating_space(space_taken, total_width_left) {\n                        break;\n                    } else {\n                        total_width_left =\n                            total_width_left.saturating_sub(space_taken + COLUMN_SPACING);\n\n                        // SAFETY: This is safe as we call `stop_allocating_space` which checks that\n                        // the value pushed is greater than zero.\n                        unsafe {\n                            calculated_widths.push(NonZeroU16::new_unchecked(space_taken));\n                        }\n                    }\n                }\n                ColumnWidthBounds::Hard(width) => {\n                    let min_width = *width;\n                    if stop_allocating_space(min_width, total_width_left) {\n                        break;\n                    } else {\n                        total_width_left =\n                            total_width_left.saturating_sub(min_width + COLUMN_SPACING);\n\n                        // SAFETY: This is safe as we call `stop_allocating_space` which checks that\n                        // the value pushed is greater than zero.\n                        unsafe {\n                            calculated_widths.push(NonZeroU16::new_unchecked(min_width));\n                        }\n                    }\n                }\n                ColumnWidthBounds::FollowHeader => {\n                    let min_width = column.header_len() as u16;\n                    if stop_allocating_space(min_width, total_width_left) {\n                        break;\n                    } else {\n                        total_width_left =\n                            total_width_left.saturating_sub(min_width + COLUMN_SPACING);\n\n                        // SAFETY: This is safe as we call `stop_allocating_space` which checks that\n                        // the value pushed is greater than zero.\n                        unsafe {\n                            calculated_widths.push(NonZeroU16::new_unchecked(min_width));\n                        }\n                    }\n                }\n            }\n        }\n\n        if !calculated_widths.is_empty() {\n            if !left_to_right {\n                calculated_widths.reverse();\n            }\n\n            // Redistribute remaining space.\n            let mut num_dist = calculated_widths.len() as u16;\n            let amount_per_slot = total_width_left / num_dist; // Safe from DBZ by above empty check.\n            total_width_left %= num_dist;\n\n            for width in calculated_widths.iter_mut() {\n                if num_dist == 0 {\n                    break;\n                }\n\n                if total_width_left > 0 {\n                    *width = width.saturating_add(amount_per_slot + 1);\n                    total_width_left -= 1;\n                } else {\n                    *width = width.saturating_add(amount_per_slot);\n                }\n\n                num_dist -= 1;\n            }\n        }\n\n        calculated_widths\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/data_table/data_type.rs",
    "content": "use std::{borrow::Cow, num::NonZeroU16};\n\nuse tui::widgets::Row;\n\nuse super::{ColumnHeader, DataTableColumn};\nuse crate::canvas::Painter;\n\npub trait DataToCell<H>\nwhere\n    H: ColumnHeader,\n{\n    /// Given data, a column, and its corresponding width, return the string in\n    /// the cell that will be displayed in the [`super::DataTable`].\n    fn to_cell_text(&self, column: &H, calculated_width: NonZeroU16) -> Option<Cow<'static, str>>;\n\n    /// Given a column, how to style a cell if one needs to override the default styling.\n    ///\n    /// By default this just returns [`None`], deferring to the row or table styling.\n    #[expect(\n        unused_variables,\n        reason = \"The default implementation just returns `None`.\"\n    )]\n    fn style_cell(&self, column: &H, painter: &Painter) -> Option<tui::style::Style> {\n        None\n    }\n\n    /// Apply styling to the generated [`Row`] of cells.\n    ///\n    /// The default implementation just returns the `row` that is passed in.\n    #[inline(always)]\n    #[expect(\n        unused_variables,\n        reason = \"The default implementation just returns an unstyled row.\"\n    )]\n    fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {\n        row\n    }\n\n    /// Returns the desired column widths in light of having seen data.\n    fn column_widths<C: DataTableColumn<H>>(data: &[Self], columns: &[C]) -> Vec<u16>\n    where\n        Self: Sized;\n}\n"
  },
  {
    "path": "src/canvas/components/data_table/draw.rs",
    "content": "use std::{\n    cmp::{max, min},\n    iter::once,\n};\n\nuse concat_string::concat_string;\nuse tui::{\n    Frame,\n    layout::{Constraint, Direction, Layout, Rect},\n    text::{Line, Span, Text},\n    widgets::{Block, Cell, Row, Table},\n};\n\nuse super::{\n    CalculateColumnWidths, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataToCell,\n    SortType,\n};\nuse crate::{\n    app::layout_manager::BottomWidget,\n    canvas::{Painter, drawing_utils::widget_block},\n    constants::TABLE_GAP_HEIGHT_LIMIT,\n    utils::strings::truncate_to_text,\n};\n\npub enum SelectionState {\n    NotSelected,\n    Selected,\n    Expanded,\n}\n\nimpl SelectionState {\n    pub fn new(is_expanded: bool, is_on_widget: bool) -> Self {\n        if is_expanded {\n            SelectionState::Expanded\n        } else if is_on_widget {\n            SelectionState::Selected\n        } else {\n            SelectionState::NotSelected\n        }\n    }\n}\n\n/// A [`DrawInfo`] is information required on each draw call.\npub struct DrawInfo {\n    pub loc: Rect,\n    pub force_redraw: bool,\n    pub recalculate_column_widths: bool,\n    pub selection_state: SelectionState,\n}\n\nimpl DrawInfo {\n    pub fn is_on_widget(&self) -> bool {\n        matches!(self.selection_state, SelectionState::Selected)\n            || matches!(self.selection_state, SelectionState::Expanded)\n    }\n\n    pub fn is_expanded(&self) -> bool {\n        matches!(self.selection_state, SelectionState::Expanded)\n    }\n}\n\nimpl<DataType, H, S, C> DataTable<DataType, H, S, C>\nwhere\n    DataType: DataToCell<H>,\n    H: ColumnHeader,\n    S: SortType,\n    C: DataTableColumn<H>,\n{\n    fn block<'a>(&self, draw_info: &'a DrawInfo, data_len: usize) -> Block<'a> {\n        let is_selected = match draw_info.selection_state {\n            SelectionState::NotSelected => false,\n            SelectionState::Selected | SelectionState::Expanded => true,\n        };\n\n        let border_style = if is_selected {\n            self.styling.highlighted_border_style\n        } else {\n            self.styling.border_style\n        };\n\n        let mut block = widget_block(self.props.is_basic, is_selected, self.styling.border_type)\n            .border_style(border_style);\n\n        if let Some((left_title, right_title)) = self.generate_title(draw_info, data_len) {\n            if !self.props.is_basic {\n                block = block.title_top(left_title);\n            }\n\n            if let Some(right_title) = right_title {\n                block = block.title_top(right_title);\n            }\n        }\n\n        block\n    }\n\n    /// Generates a title, given the available space.\n    fn generate_title(\n        &self, draw_info: &'_ DrawInfo, total_items: usize,\n    ) -> Option<(Line<'static>, Option<Line<'static>>)> {\n        self.props.title.as_ref().map(|title| {\n            let current_index = self.state.current_index.saturating_add(1);\n            let draw_loc = draw_info.loc;\n            let title_style = self.styling.title_style;\n\n            let title = if self.props.show_table_scroll_position {\n                let pos = current_index.to_string();\n                let tot = total_items.to_string();\n                let title_string = concat_string!(title, \"(\", pos, \" of \", tot, \") \");\n\n                if title_string.len() + 2 <= draw_loc.width.into() {\n                    title_string\n                } else {\n                    title.to_string()\n                }\n            } else {\n                title.to_string()\n            };\n\n            let left_title = Line::from(Span::styled(title, title_style)).left_aligned();\n\n            let right_title = if draw_info.is_expanded() {\n                Some(Line::from(\" Esc to go back \").right_aligned())\n            } else {\n                None\n            };\n\n            (left_title, right_title)\n        })\n    }\n\n    pub fn draw(\n        &mut self, f: &mut Frame<'_>, draw_info: &DrawInfo, widget: Option<&mut BottomWidget>,\n        painter: &Painter,\n    ) {\n        let draw_loc = draw_info.loc;\n        let margined_draw_loc = Layout::default()\n            .constraints([Constraint::Percentage(100)])\n            .horizontal_margin(u16::from(self.props.is_basic && !draw_info.is_on_widget()))\n            .direction(Direction::Horizontal)\n            .split(draw_loc)[0];\n\n        let block = self.block(draw_info, self.data.len());\n\n        let (inner_width, inner_height) = {\n            let inner_rect = block.inner(margined_draw_loc);\n            self.state.inner_rect = inner_rect;\n            (inner_rect.width, inner_rect.height)\n        };\n\n        if inner_width == 0 || inner_height == 0 {\n            f.render_widget(block, margined_draw_loc);\n        } else {\n            // Calculate widths\n            if draw_info.recalculate_column_widths {\n                let col_widths = DataType::column_widths(&self.data, &self.columns);\n\n                self.columns\n                    .iter_mut()\n                    .zip(&col_widths)\n                    .for_each(|(column, &width)| {\n                        let header_len = column.header_len() as u16;\n                        if let ColumnWidthBounds::Soft {\n                            desired,\n                            max_percentage: _,\n                        } = &mut column.bounds_mut()\n                        {\n                            *desired = max(header_len, width);\n                        }\n                    });\n\n                self.state.calculated_widths = self\n                    .columns\n                    .calculate_column_widths(inner_width, self.props.left_to_right);\n\n                // Update draw loc in widget map\n                if let Some(widget) = widget {\n                    widget.top_left_corner = Some((draw_loc.x, draw_loc.y));\n                    widget.bottom_right_corner =\n                        Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));\n                }\n            }\n\n            let show_header = inner_height > 1;\n            let header_height = u16::from(show_header);\n            let table_gap = if !show_header || draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {\n                0\n            } else {\n                self.props.table_gap\n            };\n\n            if !self.data.is_empty() || !self.first_draw {\n                if self.first_draw {\n                    // TODO: Doing it this way is fine, but it could be done better (e.g. showing\n                    // custom no results/entries message)\n                    self.first_draw = false;\n                    if let Some(first_index) = self.first_index {\n                        self.set_position(first_index);\n                    }\n                }\n\n                let columns = &self.columns;\n                let rows = {\n                    let num_rows =\n                        usize::from(inner_height.saturating_sub(table_gap + header_height));\n                    self.state\n                        .get_start_position(num_rows, draw_info.force_redraw);\n                    let start = self.state.display_start_index;\n                    let end = min(self.data.len(), start + num_rows);\n                    self.state\n                        .table_state\n                        .select(Some(self.state.current_index.saturating_sub(start)));\n\n                    self.data[start..end].iter().map(|data_row| {\n                        let row = Row::new(\n                            columns\n                                .iter()\n                                .zip(&self.state.calculated_widths)\n                                .filter_map(|(column, &width)| {\n                                    data_row.to_cell_text(column.inner(), width).map(|content| {\n                                        let content = truncate_to_text(&content, width.get());\n\n                                        if let Some(style) =\n                                            data_row.style_cell(column.inner(), painter)\n                                        {\n                                            Cell::new(content).style(style)\n                                        } else {\n                                            Cell::new(content)\n                                        }\n                                    })\n                                }),\n                        );\n\n                        data_row.style_row(row, painter)\n                    })\n                };\n\n                let headers = self\n                    .sort_type\n                    .build_header(columns, &self.state.calculated_widths)\n                    .style(self.styling.header_style)\n                    .bottom_margin(table_gap);\n\n                let widget = {\n                    let highlight_style = if draw_info.is_on_widget()\n                        || self.props.show_current_entry_when_unfocused\n                    {\n                        self.styling.highlighted_text_style\n                    } else {\n                        self.styling.text_style\n                    };\n                    let mut table = Table::new(\n                        rows,\n                        self.state.calculated_widths.iter().map(|nzu| nzu.get()),\n                    )\n                    .block(block)\n                    .row_highlight_style(highlight_style)\n                    .style(self.styling.text_style);\n\n                    if show_header {\n                        table = table.header(headers);\n                    }\n\n                    table\n                };\n\n                let table_state = &mut self.state.table_state;\n                f.render_stateful_widget(widget, margined_draw_loc, table_state);\n            } else {\n                let table = Table::new(\n                    once(Row::new(Text::raw(\"No data\"))),\n                    [Constraint::Percentage(100)],\n                )\n                .block(block)\n                .style(self.styling.text_style);\n                f.render_widget(table, margined_draw_loc);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/data_table/props.rs",
    "content": "use std::borrow::Cow;\n\npub struct DataTableProps {\n    /// An optional title for the table.\n    pub title: Option<Cow<'static, str>>,\n\n    /// The size of the gap between the header and rows.\n    pub table_gap: u16,\n\n    /// Whether this table determines column widths from left to right.\n    pub left_to_right: bool,\n\n    /// Whether this table is a basic table. This affects the borders.\n    pub is_basic: bool,\n\n    /// Whether to show the table scroll position.\n    pub show_table_scroll_position: bool,\n\n    /// Whether to show the current entry as highlighted when not focused.\n    pub show_current_entry_when_unfocused: bool,\n}\n"
  },
  {
    "path": "src/canvas/components/data_table/sortable.rs",
    "content": "use std::{borrow::Cow, marker::PhantomData, num::NonZeroU16};\n\nuse concat_string::concat_string;\nuse itertools::Itertools;\nuse tui::widgets::Row;\n\nuse super::{\n    ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps, DataTableState,\n    DataTableStyling, DataToCell,\n};\nuse crate::utils::strings::truncate_to_text;\n\n/// Denotes the sort order.\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum SortOrder {\n    Ascending,\n    Descending,\n}\n\nimpl SortOrder {\n    /// Returns the reverse [`SortOrder`].\n    pub fn rev(&self) -> SortOrder {\n        match self {\n            SortOrder::Ascending => SortOrder::Descending,\n            SortOrder::Descending => SortOrder::Ascending,\n        }\n    }\n\n    /// A hack to get a const default.\n    pub const fn const_default() -> Self {\n        Self::Ascending\n    }\n}\n\nimpl Default for SortOrder {\n    fn default() -> Self {\n        Self::const_default()\n    }\n}\n\n/// Denotes the [`DataTable`] is unsorted.\npub struct Unsortable;\n\n/// Denotes the [`DataTable`] is sorted.\npub struct Sortable {\n    /// The currently selected sort index.\n    pub sort_index: usize,\n\n    /// The current sorting order.\n    pub order: SortOrder,\n}\n\n/// The [`SortType`] trait is meant to be used in the typing of a [`DataTable`]\n/// to denote whether the table is meant to display/store sorted or unsorted\n/// data.\n///\n/// Note that the trait is [sealed](https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed),\n/// and therefore only [`Unsortable`] and [`Sortable`] can implement it.\npub trait SortType: private::Sealed {\n    /// Constructs the table header.\n    fn build_header<H, C>(&self, columns: &[C], widths: &[NonZeroU16]) -> Row<'_>\n    where\n        H: ColumnHeader,\n        C: DataTableColumn<H>,\n    {\n        Row::new(\n            columns\n                .iter()\n                .zip(widths)\n                .map(|(c, &width)| truncate_to_text(&c.header(), width.get())),\n        )\n    }\n}\n\nmod private {\n    use super::{Sortable, Unsortable};\n\n    pub trait Sealed {}\n\n    impl Sealed for Unsortable {}\n    impl Sealed for Sortable {}\n}\n\nimpl SortType for Unsortable {}\n\nimpl SortType for Sortable {\n    fn build_header<H, C>(&self, columns: &[C], widths: &[NonZeroU16]) -> Row<'_>\n    where\n        H: ColumnHeader,\n        C: DataTableColumn<H>,\n    {\n        const UP_ARROW: &str = \"▲\";\n        const DOWN_ARROW: &str = \"▼\";\n\n        Row::new(\n            columns\n                .iter()\n                .zip(widths)\n                .enumerate()\n                .map(|(index, (c, &width))| {\n                    if index == self.sort_index {\n                        let arrow = match self.order {\n                            SortOrder::Ascending => UP_ARROW,\n                            SortOrder::Descending => DOWN_ARROW,\n                        };\n                        // TODO: I think I can get away with removing the truncate_to_text call\n                        // since I almost always bind to at least the header\n                        // size... TODO: Or should we instead truncate but\n                        // ALWAYS leave the arrow at the end?\n                        truncate_to_text(&concat_string!(c.header(), arrow), width.get())\n                    } else {\n                        truncate_to_text(&c.header(), width.get())\n                    }\n                }),\n        )\n    }\n}\n\npub trait SortsRow {\n    type DataType;\n\n    /// Sorts data.\n    fn sort_data(&self, data: &mut [Self::DataType], descending: bool);\n}\n\n#[derive(Debug, Clone)]\npub struct SortColumn<T> {\n    /// The inner column header.\n    inner: T,\n\n    /// The default sort order.\n    pub default_order: SortOrder,\n\n    /// A restriction on this column's width.\n    pub bounds: ColumnWidthBounds,\n\n    /// Marks that this column is currently \"hidden\", and should *always* be\n    /// skipped.\n    pub is_hidden: bool,\n}\n\nimpl<D, T> DataTableColumn<T> for SortColumn<T>\nwhere\n    T: ColumnHeader + SortsRow<DataType = D>,\n{\n    #[inline]\n    fn inner(&self) -> &T {\n        &self.inner\n    }\n\n    #[inline]\n    fn inner_mut(&mut self) -> &mut T {\n        &mut self.inner\n    }\n\n    #[inline]\n    fn bounds(&self) -> ColumnWidthBounds {\n        self.bounds\n    }\n\n    #[inline]\n    fn bounds_mut(&mut self) -> &mut ColumnWidthBounds {\n        &mut self.bounds\n    }\n\n    #[inline]\n    fn is_hidden(&self) -> bool {\n        self.is_hidden\n    }\n\n    fn header(&self) -> Cow<'static, str> {\n        self.inner.header()\n    }\n\n    fn header_len(&self) -> usize {\n        self.header().len() + 1\n    }\n}\n\nimpl<D, T> SortColumn<T>\nwhere\n    T: ColumnHeader + SortsRow<DataType = D>,\n{\n    /// Creates a new [`SortColumn`] with a width that follows the header width,\n    /// which has no shortcut and sorts by default in ascending order\n    /// ([`SortOrder::Ascending`]).\n    pub fn new(inner: T) -> Self {\n        Self {\n            inner,\n            bounds: ColumnWidthBounds::FollowHeader,\n            is_hidden: false,\n            default_order: SortOrder::default(),\n        }\n    }\n\n    /// Creates a new [`SortColumn`] with a hard width, which has no shortcut\n    /// and sorts by default in ascending order ([`SortOrder::Ascending`]).\n    pub const fn hard(inner: T, width: u16) -> Self {\n        Self {\n            inner,\n            bounds: ColumnWidthBounds::Hard(width),\n            is_hidden: false,\n            default_order: SortOrder::const_default(),\n        }\n    }\n\n    /// Creates a new [`SortColumn`] with a soft width, which has no shortcut\n    /// and sorts by default in ascending order ([`SortOrder::Ascending`]).\n    pub const fn soft(inner: T, max_percentage: Option<f32>) -> Self {\n        Self {\n            inner,\n            bounds: ColumnWidthBounds::Soft {\n                desired: 0,\n                max_percentage,\n            },\n            is_hidden: false,\n            default_order: SortOrder::const_default(),\n        }\n    }\n\n    /// Sets the default sort order to [`SortOrder::Descending`].\n    pub const fn default_descending(mut self) -> Self {\n        self.default_order = SortOrder::Descending;\n        self\n    }\n\n    /// Given a [`SortColumn`] and the sort order, sort a mutable slice of\n    /// associated data.\n    pub fn sort_by(&self, data: &mut [D], order: SortOrder) {\n        let descending = matches!(order, SortOrder::Descending);\n        self.inner.sort_data(data, descending);\n    }\n}\n\npub struct SortDataTableProps {\n    pub inner: DataTableProps,\n    pub sort_index: usize,\n    pub order: SortOrder,\n}\n\n/// A type alias for a sortable [`DataTable`].\npub type SortDataTable<DataType, H> = DataTable<DataType, H, Sortable, SortColumn<H>>;\n\nimpl<D, H> SortDataTable<D, H>\nwhere\n    D: DataToCell<H>,\n    H: ColumnHeader + SortsRow<DataType = D>,\n{\n    pub fn new_sortable<C: Into<Vec<SortColumn<H>>>>(\n        columns: C, props: SortDataTableProps, styling: DataTableStyling,\n    ) -> Self {\n        Self {\n            columns: columns.into(),\n            state: DataTableState::default(),\n            props: props.inner,\n            styling,\n            sort_type: Sortable {\n                sort_index: props.sort_index,\n                order: props.order,\n            },\n            first_draw: true,\n            first_index: None,\n            data: vec![],\n            _pd: PhantomData,\n        }\n    }\n\n    /// Sets the current sort order.\n    pub fn set_order(&mut self, order: SortOrder) {\n        self.sort_type.order = order;\n    }\n\n    /// Gets the current sort order.\n    pub fn order(&self) -> SortOrder {\n        self.sort_type.order\n    }\n\n    /// Toggles the current sort order.\n    pub fn toggle_order(&mut self) {\n        self.sort_type.order = match self.sort_type.order {\n            SortOrder::Ascending => SortOrder::Descending,\n            SortOrder::Descending => SortOrder::Ascending,\n        }\n    }\n\n    /// Given some `x` and `y`, if possible, select the corresponding column or\n    /// toggle the column if already selected, and otherwise do nothing.\n    ///\n    /// If there was some update, the corresponding column type will be\n    /// returned. If nothing happens, [`None`] is returned.\n    pub fn try_select_location(&mut self, x: u16, y: u16) -> Option<usize> {\n        if self.state.inner_rect.height > 1 && self.state.inner_rect.y == y {\n            if let Some(index) = self.get_range(x) {\n                self.set_sort_index(index);\n                Some(self.sort_type.sort_index)\n            } else {\n                None\n            }\n        } else {\n            None\n        }\n    }\n\n    /// Updates the sort index, and sets the sort order as appropriate.\n    ///\n    /// If the index is different from the previous one, it will move to the new\n    /// index and set the sort order to the prescribed default sort order.\n    ///\n    /// If the index is the same as the previous one, it will simply toggle the\n    /// current sort order.\n    pub fn set_sort_index(&mut self, index: usize) {\n        if self.sort_type.sort_index == index {\n            self.toggle_order();\n        } else if let Some(col) = self.columns.get(index) {\n            self.sort_type.sort_index = index;\n            self.sort_type.order = col.default_order;\n        }\n    }\n\n    /// Returns the current sort index.\n    pub fn sort_index(&self) -> usize {\n        self.sort_type.sort_index\n    }\n\n    /// Given a `needle` coordinate, select the corresponding index and value.\n    fn get_range(&self, needle: u16) -> Option<usize> {\n        let mut start = self.state.inner_rect.x;\n        let range = self\n            .state\n            .calculated_widths\n            .iter()\n            .map(|width| {\n                let entry_start = start;\n                start += width.get() + 1; // +1 for the gap b/w cols.\n\n                entry_start\n            })\n            .collect_vec();\n\n        match range.binary_search(&needle) {\n            Ok(index) => Some(index),\n            Err(index) => index.checked_sub(1),\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[derive(Clone, PartialEq, Eq, Debug)]\n    struct TestType {\n        index: usize,\n        data: u64,\n    }\n\n    enum ColumnType {\n        Index,\n        Data,\n    }\n\n    impl DataToCell<ColumnType> for TestType {\n        fn to_cell_text(\n            &self, _column: &ColumnType, _calculated_width: NonZeroU16,\n        ) -> Option<Cow<'static, str>> {\n            None\n        }\n\n        fn column_widths<C: DataTableColumn<ColumnType>>(_data: &[Self], _columns: &[C]) -> Vec<u16>\n        where\n            Self: Sized,\n        {\n            vec![]\n        }\n    }\n\n    impl ColumnHeader for ColumnType {\n        fn text(&self) -> Cow<'static, str> {\n            match self {\n                ColumnType::Index => \"Index\".into(),\n                ColumnType::Data => \"Data\".into(),\n            }\n        }\n    }\n\n    impl SortsRow for ColumnType {\n        type DataType = TestType;\n\n        fn sort_data(&self, data: &mut [TestType], descending: bool) {\n            match self {\n                ColumnType::Index => data.sort_by_key(|t| t.index),\n                ColumnType::Data => data.sort_by_key(|t| t.data),\n            }\n\n            if descending {\n                data.reverse();\n            }\n        }\n    }\n\n    #[test]\n    fn test_sorting() {\n        let columns = [\n            SortColumn::new(ColumnType::Index),\n            SortColumn::new(ColumnType::Data),\n        ];\n        let props = {\n            let inner = DataTableProps {\n                title: Some(\"test\".into()),\n                table_gap: 1,\n                left_to_right: false,\n                is_basic: false,\n                show_table_scroll_position: true,\n                show_current_entry_when_unfocused: false,\n            };\n\n            SortDataTableProps {\n                inner,\n                sort_index: 0,\n                order: SortOrder::Descending,\n            }\n        };\n\n        let styling = DataTableStyling::default();\n\n        let mut table = DataTable::new_sortable(columns, props, styling);\n        let mut data = vec![\n            TestType {\n                index: 4,\n                data: 100,\n            },\n            TestType {\n                index: 1,\n                data: 200,\n            },\n            TestType {\n                index: 0,\n                data: 300,\n            },\n            TestType {\n                index: 3,\n                data: 400,\n            },\n            TestType {\n                index: 2,\n                data: 500,\n            },\n        ];\n\n        table\n            .columns\n            .get(table.sort_type.sort_index)\n            .unwrap()\n            .sort_by(&mut data, SortOrder::Ascending);\n        assert_eq!(\n            data,\n            vec![\n                TestType {\n                    index: 0,\n                    data: 300,\n                },\n                TestType {\n                    index: 1,\n                    data: 200,\n                },\n                TestType {\n                    index: 2,\n                    data: 500,\n                },\n                TestType {\n                    index: 3,\n                    data: 400,\n                },\n                TestType {\n                    index: 4,\n                    data: 100,\n                },\n            ]\n        );\n\n        table\n            .columns\n            .get(table.sort_type.sort_index)\n            .unwrap()\n            .sort_by(&mut data, SortOrder::Descending);\n        assert_eq!(\n            data,\n            vec![\n                TestType {\n                    index: 4,\n                    data: 100,\n                },\n                TestType {\n                    index: 3,\n                    data: 400,\n                },\n                TestType {\n                    index: 2,\n                    data: 500,\n                },\n                TestType {\n                    index: 1,\n                    data: 200,\n                },\n                TestType {\n                    index: 0,\n                    data: 300,\n                },\n            ]\n        );\n\n        table.set_sort_index(1);\n        table\n            .columns\n            .get(table.sort_type.sort_index)\n            .unwrap()\n            .sort_by(&mut data, SortOrder::Ascending);\n        assert_eq!(\n            data,\n            vec![\n                TestType {\n                    index: 4,\n                    data: 100,\n                },\n                TestType {\n                    index: 1,\n                    data: 200,\n                },\n                TestType {\n                    index: 0,\n                    data: 300,\n                },\n                TestType {\n                    index: 3,\n                    data: 400,\n                },\n                TestType {\n                    index: 2,\n                    data: 500,\n                },\n            ]\n        );\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/data_table/state.rs",
    "content": "use std::num::NonZeroU16;\n\nuse tui::{layout::Rect, widgets::TableState};\n\n#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]\npub enum ScrollDirection {\n    // UP means scrolling up --- this usually DECREMENTS\n    Up,\n\n    // DOWN means scrolling down --- this usually INCREMENTS\n    #[default]\n    Down,\n}\n\n/// Internal state representation of a [`DataTable`](super::DataTable).\npub struct DataTableState {\n    /// The index from where to start displaying the rows.\n    pub display_start_index: usize,\n\n    /// The current scroll position.\n    pub current_index: usize,\n\n    /// The direction of the last attempted scroll.\n    pub scroll_direction: ScrollDirection,\n\n    /// ratatui's internal table state.\n    pub table_state: TableState,\n\n    /// The calculated widths.\n    pub calculated_widths: Vec<NonZeroU16>,\n\n    /// The current inner [`Rect`].\n    pub inner_rect: Rect,\n}\n\nimpl Default for DataTableState {\n    fn default() -> Self {\n        Self {\n            display_start_index: 0,\n            current_index: 0,\n            scroll_direction: ScrollDirection::Down,\n            calculated_widths: vec![],\n            table_state: TableState::default(),\n            inner_rect: Rect::default(),\n        }\n    }\n}\n\nimpl DataTableState {\n    /// Gets the starting position of a table.\n    pub fn get_start_position(&mut self, num_rows: usize, is_force_redraw: bool) {\n        let start_index = if is_force_redraw {\n            0\n        } else {\n            self.display_start_index\n        };\n        let current_scroll_position = self.current_index;\n        let scroll_direction = self.scroll_direction;\n\n        self.display_start_index = match scroll_direction {\n            ScrollDirection::Down => {\n                if current_scroll_position < start_index + num_rows {\n                    // If, using the current scroll position, we can see the element\n                    // (so within that and + num_rows) just reuse the current previously\n                    // scrolled position.\n                    start_index\n                } else if current_scroll_position >= num_rows {\n                    // If the current position past the last element visible in the list,\n                    // then skip until we can see that element.\n                    current_scroll_position - num_rows + 1\n                } else {\n                    // Else, if it is not past the last element visible, do not omit anything.\n                    0\n                }\n            }\n            ScrollDirection::Up => {\n                if current_scroll_position <= start_index {\n                    // If it's past the first element, then show from that element downwards\n                    current_scroll_position\n                } else if current_scroll_position >= start_index + num_rows {\n                    current_scroll_position - num_rows + 1\n                } else {\n                    start_index\n                }\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/data_table/styling.rs",
    "content": "use tui::{style::Style, widgets::BorderType};\n\nuse crate::options::config::style::Styles;\n\n#[derive(Default)]\npub struct DataTableStyling {\n    pub header_style: Style,\n    pub border_style: Style,\n    pub border_type: BorderType,\n    pub highlighted_border_style: Style,\n    pub text_style: Style,\n    pub highlighted_text_style: Style,\n    pub title_style: Style,\n}\n\nimpl DataTableStyling {\n    pub fn from_palette(styles: &Styles) -> Self {\n        Self {\n            header_style: styles.table_header_style,\n            border_style: styles.border_style,\n            border_type: styles.border_type,\n            highlighted_border_style: styles.highlighted_border_style,\n            text_style: styles.text_style,\n            highlighted_text_style: styles.selected_text_style,\n            title_style: styles.widget_title_style,\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/data_table.rs",
    "content": "pub mod column;\npub mod data_type;\npub mod draw;\npub mod props;\npub mod sortable;\npub mod state;\npub mod styling;\n\nuse std::{convert::TryInto, marker::PhantomData};\n\npub use column::*;\npub use data_type::*;\npub use draw::*;\npub use props::DataTableProps;\npub use sortable::*;\npub use state::{DataTableState, ScrollDirection};\npub use styling::*;\n\nuse crate::utils::general::ClampExt;\n\n/// A [`DataTable`] is a component that displays data in a tabular form.\n///\n/// Note that [`DataTable`] takes a generic type `S`, bounded by [`SortType`].\n/// This controls whether this table expects sorted data or not, with two\n/// expected types:\n///\n/// - [`Unsortable`]: The default if otherwise not specified. This table does\n///   not expect sorted data.\n/// - [`Sortable`]: This table expects sorted data, and there are helper\n///   functions to facilitate things like sorting based on a selected column,\n///   shortcut column selection support, mouse column selection support, etc.\n///\n/// FIXME: We already do all the text width checks - can we skip the underlying ones?\npub struct DataTable<DataType, Header, S = Unsortable, C = Column<Header>> {\n    pub columns: Vec<C>,\n    pub state: DataTableState,\n    pub props: DataTableProps,\n    pub styling: DataTableStyling,\n    data: Vec<DataType>,\n    sort_type: S,\n    first_draw: bool,\n    first_index: Option<usize>,\n    _pd: PhantomData<(DataType, S, Header)>,\n}\n\nimpl<DataType: DataToCell<H>, H: ColumnHeader> DataTable<DataType, H, Unsortable, Column<H>> {\n    pub fn new<C: Into<Vec<Column<H>>>>(\n        columns: C, props: DataTableProps, styling: DataTableStyling,\n    ) -> Self {\n        Self {\n            columns: columns.into(),\n            state: DataTableState::default(),\n            props,\n            styling,\n            data: vec![],\n            sort_type: Unsortable,\n            first_draw: true,\n            first_index: None,\n            _pd: PhantomData,\n        }\n    }\n}\n\nimpl<DataType: DataToCell<H>, H: ColumnHeader, S: SortType, C: DataTableColumn<H>>\n    DataTable<DataType, H, S, C>\n{\n    /// Sets the default value selected on first initialization, if possible.\n    pub fn first_draw_index(mut self, first_index: usize) -> Self {\n        self.first_index = Some(first_index);\n        self\n    }\n\n    /// Sets the scroll position to the first value.\n    pub fn scroll_to_first(&mut self) {\n        self.state.current_index = 0;\n        self.state.scroll_direction = ScrollDirection::Up;\n    }\n\n    /// Sets the scroll position to the last value.\n    pub fn scroll_to_last(&mut self) {\n        self.state.current_index = self.data.len().saturating_sub(1);\n        self.state.scroll_direction = ScrollDirection::Down;\n    }\n\n    /// Updates the scroll position to be valid for the number of entries.\n    pub fn set_data(&mut self, data: Vec<DataType>) {\n        self.data = data;\n        let max_pos = self.data.len().saturating_sub(1);\n        if self.state.current_index > max_pos {\n            self.state.current_index = max_pos;\n            self.state.display_start_index = 0;\n            self.state.scroll_direction = ScrollDirection::Down;\n        }\n    }\n\n    /// Increments the scroll position if possible by a positive/negative\n    /// offset. If there is a valid change, this function will also return\n    /// the new position wrapped in an [`Option`].\n    ///\n    /// Note that despite the name, this handles both incrementing (positive\n    /// change) and decrementing (negative change).\n    pub fn increment_position(&mut self, change: i64) -> Option<usize> {\n        let num_entries = self.data.len();\n\n        if num_entries == 0 {\n            return None;\n        }\n\n        let Ok(current_index): Result<i64, _> = self.state.current_index.try_into() else {\n            return None;\n        };\n\n        // We do this to clamp the proposed index to 0 if the change is greater\n        // than the number of entries left from the current index. This gives\n        // a more intuitive behaviour when using things like page up/down.\n        let proposed = current_index + change;\n\n        // We check num_entries > 0 above.\n        self.state.current_index = proposed.clamp(0, (num_entries - 1) as i64) as usize;\n\n        self.state.scroll_direction = if change < 0 {\n            ScrollDirection::Up\n        } else {\n            ScrollDirection::Down\n        };\n\n        Some(self.state.current_index)\n    }\n\n    /// Updates the scroll position to a selected index.\n    pub fn set_position(&mut self, new_index: usize) {\n        let new_index = new_index.clamp_upper(self.data.len().saturating_sub(1));\n        if self.state.current_index < new_index {\n            self.state.scroll_direction = ScrollDirection::Down;\n        } else if self.state.current_index > new_index {\n            self.state.scroll_direction = ScrollDirection::Up;\n        }\n        self.state.current_index = new_index;\n    }\n\n    /// Returns the current scroll index.\n    pub fn current_index(&self) -> usize {\n        self.state.current_index\n    }\n\n    /// Optionally returns the currently selected item, if there is one.\n    pub fn current_item(&self) -> Option<&DataType> {\n        self.data.get(self.state.current_index)\n    }\n\n    /// Returns ratatui's internal selection.\n    pub fn ratatui_selected(&self) -> Option<usize> {\n        self.state.table_state.selected()\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use std::{borrow::Cow, num::NonZeroU16};\n\n    use super::*;\n\n    #[derive(Clone, PartialEq, Eq, Debug)]\n    struct TestType {\n        index: usize,\n    }\n\n    impl DataToCell<&'static str> for TestType {\n        fn to_cell_text(\n            &self, _column: &&'static str, _calculated_width: NonZeroU16,\n        ) -> Option<Cow<'static, str>> {\n            None\n        }\n\n        fn column_widths<C: DataTableColumn<&'static str>>(\n            _data: &[Self], _columns: &[C],\n        ) -> Vec<u16>\n        where\n            Self: Sized,\n        {\n            vec![]\n        }\n    }\n\n    fn create_test_table() -> DataTable<TestType, &'static str> {\n        let columns = [Column::hard(\"a\", 10), Column::hard(\"b\", 10)];\n        let props = DataTableProps {\n            title: Some(\"test\".into()),\n            table_gap: 1,\n            left_to_right: false,\n            is_basic: false,\n            show_table_scroll_position: true,\n            show_current_entry_when_unfocused: false,\n        };\n        let styling = DataTableStyling::default();\n\n        DataTable::new(columns, props, styling)\n    }\n\n    #[test]\n    fn test_scrolling() {\n        let mut table = create_test_table();\n        table.set_data((0..=4).map(|index| TestType { index }).collect::<Vec<_>>());\n\n        table.scroll_to_last();\n        assert_eq!(table.current_index(), 4);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);\n\n        table.scroll_to_first();\n        assert_eq!(table.current_index(), 0);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Up);\n    }\n\n    #[test]\n    fn test_set_position() {\n        let mut table = create_test_table();\n        table.set_data((0..=4).map(|index| TestType { index }).collect::<Vec<_>>());\n\n        table.set_position(4);\n        assert_eq!(table.current_index(), 4);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);\n\n        table.set_position(100);\n        assert_eq!(table.current_index(), 4);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);\n        assert_eq!(table.current_item(), Some(&TestType { index: 4 }));\n    }\n\n    #[test]\n    fn test_increment_position() {\n        let mut table = create_test_table();\n        table.set_data((0..=4).map(|index| TestType { index }).collect::<Vec<_>>());\n\n        table.set_position(4);\n        assert_eq!(table.current_index(), 4);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);\n\n        table.increment_position(-1);\n        assert_eq!(table.current_index(), 3);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Up);\n        assert_eq!(table.current_item(), Some(&TestType { index: 3 }));\n\n        table.increment_position(-3);\n        assert_eq!(table.current_index(), 0);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Up);\n        assert_eq!(table.current_item(), Some(&TestType { index: 0 }));\n\n        table.increment_position(-3);\n        assert_eq!(table.current_index(), 0);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Up);\n        assert_eq!(table.current_item(), Some(&TestType { index: 0 }));\n\n        table.increment_position(1);\n        assert_eq!(table.current_index(), 1);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);\n        assert_eq!(table.current_item(), Some(&TestType { index: 1 }));\n\n        table.increment_position(3);\n        assert_eq!(table.current_index(), 4);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);\n        assert_eq!(table.current_item(), Some(&TestType { index: 4 }));\n\n        table.increment_position(10);\n        assert_eq!(table.current_index(), 4);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);\n        assert_eq!(table.current_item(), Some(&TestType { index: 4 }));\n\n        // Make sure that overscrolling up causes clamping.\n        table.increment_position(-10);\n        assert_eq!(table.current_index(), 0);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Up);\n        assert_eq!(table.current_item(), Some(&TestType { index: 0 }));\n\n        // Make sure that overscrolling down causes clamping.\n        table.increment_position(100);\n        assert_eq!(table.current_index(), 4);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);\n        assert_eq!(table.current_item(), Some(&TestType { index: 4 }));\n    }\n\n    /// A test to ensure that scroll offsets are correctly handled when we \"lose\" rows.\n    #[test]\n    fn test_lose_data() {\n        let mut table = create_test_table();\n        table.set_data((0..=4).map(|index| TestType { index }).collect::<Vec<_>>());\n\n        table.set_position(4);\n        assert_eq!(table.current_index(), 4);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);\n        assert_eq!(table.current_item(), Some(&TestType { index: 4 }));\n\n        table.set_data((0..=2).map(|index| TestType { index }).collect::<Vec<_>>());\n        assert_eq!(table.current_index(), 2);\n        assert_eq!(table.state.scroll_direction, ScrollDirection::Down);\n        assert_eq!(table.current_item(), Some(&TestType { index: 2 }));\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/mod.rs",
    "content": "//! Lower-level or shared drawing components used throughout bottom.\n\npub mod data_table;\npub mod pipe_gauge;\npub mod time_graph;\npub mod widget_carousel;\n"
  },
  {
    "path": "src/canvas/components/pipe_gauge.rs",
    "content": "use tui::{\n    buffer::Buffer,\n    layout::Rect,\n    style::Style,\n    text::Line,\n    widgets::{Block, Widget},\n};\n\n#[derive(Debug, Clone, Copy, Default)]\npub enum LabelLimit {\n    #[default]\n    None,\n    #[expect(dead_code)]\n    Auto(u16),\n    Bars,\n    StartLabel,\n}\n\n/// A widget to measure something, using pipe characters ('|') as a unit.\n#[derive(Debug, Clone)]\npub struct PipeGauge<'a> {\n    block: Option<Block<'a>>,\n    ratio: f64,\n    start_label: Option<Line<'a>>,\n    inner_label: Option<Line<'a>>,\n    label_style: Style,\n    gauge_style: Style,\n    hide_parts: LabelLimit,\n}\n\nimpl Default for PipeGauge<'_> {\n    fn default() -> Self {\n        Self {\n            block: None,\n            ratio: 0.0,\n            start_label: None,\n            inner_label: None,\n            label_style: Style::default(),\n            gauge_style: Style::default(),\n            hide_parts: LabelLimit::default(),\n        }\n    }\n}\n\nimpl<'a> PipeGauge<'a> {\n    /// The ratio, a value from 0.0 to 1.0 (any other greater or less will be\n    /// clamped) represents the portion of the pipe gauge to fill.\n    ///\n    /// Note: passing in NaN will potentially cause problems.\n    pub fn ratio(mut self, ratio: f64) -> Self {\n        self.ratio = ratio.clamp(0.0, 1.0);\n\n        self\n    }\n\n    /// The label displayed before the bar.\n    pub fn start_label<T>(mut self, start_label: T) -> Self\n    where\n        T: Into<Line<'a>>,\n    {\n        self.start_label = Some(start_label.into());\n        self\n    }\n\n    /// The label displayed inside the bar.\n    pub fn inner_label<T>(mut self, inner_label: T) -> Self\n    where\n        T: Into<Line<'a>>,\n    {\n        self.inner_label = Some(inner_label.into());\n        self\n    }\n\n    /// The style of the labels.\n    pub fn label_style(mut self, label_style: Style) -> Self {\n        self.label_style = label_style;\n        self\n    }\n\n    /// The style of the gauge itself.\n    pub fn gauge_style(mut self, style: Style) -> Self {\n        self.gauge_style = style;\n        self\n    }\n\n    /// Whether to hide parts of the gauge/label if the inner label wouldn't\n    /// fit.\n    pub fn hide_parts(mut self, hide_parts: LabelLimit) -> Self {\n        self.hide_parts = hide_parts;\n        self\n    }\n}\n\nimpl Widget for PipeGauge<'_> {\n    fn render(mut self, area: Rect, buf: &mut Buffer) {\n        buf.set_style(area, self.label_style);\n        let gauge_area = match self.block.take() {\n            Some(b) => {\n                let inner_area = b.inner(area);\n                b.render(area, buf);\n                inner_area\n            }\n            None => area,\n        };\n\n        if gauge_area.height < 1 {\n            return;\n        }\n\n        let (col, row) = {\n            let inner_label_width = self\n                .inner_label\n                .as_ref()\n                .map(|l| l.width())\n                .unwrap_or_default();\n\n            let start_label_width = self\n                .start_label\n                .as_ref()\n                .map(|l| l.width())\n                .unwrap_or_default();\n\n            match self.hide_parts {\n                LabelLimit::StartLabel => {\n                    let inner_label = self.inner_label.unwrap_or_else(|| Line::from(\"\"));\n                    let _ = buf.set_line(\n                        gauge_area.left(),\n                        gauge_area.top(),\n                        &inner_label,\n                        inner_label.width() as u16,\n                    );\n\n                    // Short circuit.\n                    return;\n                }\n                LabelLimit::Auto(_)\n                    if gauge_area.width < (inner_label_width + start_label_width + 1) as u16 =>\n                {\n                    let inner_label = self.inner_label.unwrap_or_else(|| Line::from(\"\"));\n                    let _ = buf.set_line(\n                        gauge_area.left(),\n                        gauge_area.top(),\n                        &inner_label,\n                        inner_label.width() as u16,\n                    );\n\n                    // Short circuit.\n                    return;\n                }\n                _ => {\n                    let start_label = self.start_label.unwrap_or_else(|| Line::from(\"\"));\n                    buf.set_line(\n                        gauge_area.left(),\n                        gauge_area.top(),\n                        &start_label,\n                        start_label.width() as u16,\n                    )\n                }\n            }\n        };\n\n        let end_label = self.inner_label.unwrap_or_else(|| Line::from(\"\"));\n        match self.hide_parts {\n            LabelLimit::Bars => {\n                let _ = buf.set_line(\n                    gauge_area\n                        .right()\n                        .saturating_sub(end_label.width() as u16 + 1),\n                    row,\n                    &end_label,\n                    end_label.width() as u16,\n                );\n            }\n            LabelLimit::Auto(width_limit)\n                if gauge_area.right().saturating_sub(col) < width_limit =>\n            {\n                let _ = buf.set_line(\n                    gauge_area\n                        .right()\n                        .saturating_sub(end_label.width() as u16 + 1),\n                    row,\n                    &end_label,\n                    1,\n                );\n            }\n            LabelLimit::Auto(_) | LabelLimit::None => {\n                let (start, _) = buf.set_line(col, row, &Line::from(\"[\"), gauge_area.width);\n                if start >= gauge_area.right() {\n                    return;\n                }\n\n                let (end, _) = buf.set_line(\n                    (gauge_area.x + gauge_area.width).saturating_sub(1),\n                    row,\n                    &Line::from(\"]\"),\n                    gauge_area.width,\n                );\n\n                let pipe_end =\n                    start + (f64::from(end.saturating_sub(start)) * self.ratio).floor() as u16;\n                for col in start..pipe_end {\n                    if let Some(cell) = buf.cell_mut((col, row)) {\n                        cell.set_symbol(\"|\").set_style(Style {\n                            fg: self.gauge_style.fg,\n                            bg: None,\n                            add_modifier: self.gauge_style.add_modifier,\n                            sub_modifier: self.gauge_style.sub_modifier,\n                            underline_color: None,\n                        });\n                    }\n                }\n\n                if (end_label.width() as u16) < end.saturating_sub(start) {\n                    let gauge_end = gauge_area\n                        .right()\n                        .saturating_sub(end_label.width() as u16 + 1);\n                    buf.set_line(gauge_end, row, &end_label, end_label.width() as u16);\n                }\n            }\n            LabelLimit::StartLabel => unreachable!(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/time_graph/base.rs",
    "content": "use std::{borrow::Cow, time::Instant};\n\nuse concat_string::concat_string;\nuse tui::{\n    Frame,\n    layout::{Constraint, Rect},\n    style::Style,\n    symbols::Marker,\n    text::{Line, Span},\n    widgets::{BorderType, GraphType},\n};\n\nuse crate::{\n    app::data::Values,\n    canvas::{components::time_graph::*, drawing_utils::widget_block},\n};\n\n/// Represents the data required by the [`TimeGraph`].\n///\n/// TODO: We may be able to get rid of this intermediary data structure.\n#[derive(Default)]\npub(crate) struct GraphData<'a> {\n    time: &'a [Instant],\n    values: Option<&'a Values>,\n    style: Style,\n    name: Option<Cow<'a, str>>,\n}\n\nimpl<'a> GraphData<'a> {\n    pub fn time(mut self, time: &'a [Instant]) -> Self {\n        self.time = time;\n        self\n    }\n\n    pub fn values(mut self, values: &'a Values) -> Self {\n        self.values = Some(values);\n        self\n    }\n\n    pub fn style(mut self, style: Style) -> Self {\n        self.style = style;\n        self\n    }\n\n    pub fn name(mut self, name: Cow<'a, str>) -> Self {\n        self.name = Some(name);\n        self\n    }\n}\n\npub struct TimeGraph<'a> {\n    /// The min x value.\n    pub x_min: f64,\n\n    /// Whether to hide the time/x-labels.\n    pub hide_x_labels: bool,\n\n    /// The min and max y boundaries.\n    pub y_bounds: AxisBound,\n\n    /// Any y-labels.\n    pub y_labels: &'a [Cow<'a, str>],\n\n    /// The graph style.\n    pub graph_style: Style,\n\n    /// The border style.\n    pub border_style: Style,\n\n    /// The border type.\n    pub border_type: BorderType,\n\n    /// The graph title.\n    pub title: Cow<'a, str>,\n\n    /// Whether this graph is selected.\n    pub is_selected: bool,\n\n    /// Whether this graph is expanded.\n    pub is_expanded: bool,\n\n    /// The title style.\n    pub title_style: Style,\n\n    /// The legend position.\n    pub legend_position: Option<LegendPosition>,\n\n    /// Any legend constraints.\n    pub legend_constraints: Option<(Constraint, Constraint)>,\n\n    /// The marker type. Unlike ratatui's native charts, we assume\n    /// only a single type of marker.\n    pub marker: Marker,\n\n    /// The chart scaling.\n    pub scaling: ChartScaling,\n}\n\nimpl TimeGraph<'_> {\n    /// Generates the [`Axis`] for the x-axis.\n    fn generate_x_axis(&self) -> Axis<'_> {\n        // Due to how we display things, we need to adjust the time bound values.\n        let adjusted_x_bounds = AxisBound::Min(self.x_min);\n\n        if self.hide_x_labels {\n            Axis::default().bounds(adjusted_x_bounds)\n        } else {\n            let x_bound_left = ((-self.x_min) as u64 / 1000).to_string();\n            let x_bound_right = \"0s\";\n\n            let x_labels = vec![\n                Span::styled(concat_string!(x_bound_left, \"s\"), self.graph_style),\n                Span::styled(x_bound_right, self.graph_style),\n            ];\n\n            Axis::default()\n                .bounds(adjusted_x_bounds)\n                .labels(x_labels)\n                .style(self.graph_style)\n        }\n    }\n\n    /// Generates the [`Axis`] for the y-axis.\n    fn generate_y_axis(&self) -> Axis<'_> {\n        Axis::default()\n            .bounds(self.y_bounds)\n            .style(self.graph_style)\n            .labels(\n                self.y_labels\n                    .iter()\n                    .map(|label| Span::styled(label.clone(), self.graph_style))\n                    .collect(),\n            )\n    }\n\n    /// Draws a time graph at [`Rect`] location provided by `draw_loc`. A time\n    /// graph is used to display data points throughout time in the x-axis.\n    ///\n    /// This time graph:\n    /// - Draws with the higher time value on the left, and lower on the right.\n    /// - Expects a [`TimeGraph`] to be passed in, which details how to draw the\n    ///   graph.\n    /// - Expects `graph_data`, which represents *what* data to draw, and\n    ///   various details like style and optional legends.\n    pub fn draw(&self, f: &mut Frame<'_>, draw_loc: Rect, graph_data: Vec<GraphData<'_>>) {\n        // TODO: (points_rework_v1) can we reduce allocations in the underlying graph by saving some sort of state?\n\n        let x_axis = self.generate_x_axis();\n        let y_axis = self.generate_y_axis();\n        let data = graph_data.into_iter().map(create_dataset).collect();\n\n        let block = {\n            let mut b = widget_block(false, self.is_selected, self.border_type)\n                .border_style(self.border_style)\n                .title_top(Line::styled(self.title.as_ref(), self.title_style));\n\n            if self.is_expanded {\n                b = b.title_top(Line::styled(\" Esc to go back \", self.title_style).right_aligned())\n            }\n\n            b\n        };\n\n        f.render_widget(\n            TimeChart::new(data)\n                .block(block)\n                .x_axis(x_axis)\n                .y_axis(y_axis)\n                .marker(self.marker)\n                .legend_style(self.graph_style)\n                .legend_position(self.legend_position)\n                .hidden_legend_constraints(\n                    self.legend_constraints\n                        .unwrap_or(DEFAULT_LEGEND_CONSTRAINTS),\n                )\n                .scaling(self.scaling),\n            draw_loc,\n        )\n    }\n}\n\n/// Creates a new [`Dataset`].\nfn create_dataset(data: GraphData<'_>) -> Dataset<'_> {\n    let GraphData {\n        time,\n        values,\n        style,\n        name,\n    } = data;\n\n    let Some(values) = values else {\n        return Dataset::default();\n    };\n\n    let dataset = Dataset::default()\n        .style(style)\n        .data(time, values)\n        .graph_type(GraphType::Line);\n\n    if let Some(name) = name {\n        dataset.name(name)\n    } else {\n        dataset\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use std::borrow::Cow;\n\n    use tui::{\n        style::{Color, Style},\n        symbols::Marker,\n        text::Span,\n        widgets::BorderType,\n    };\n\n    use super::{AxisBound, ChartScaling, TimeGraph};\n    use crate::canvas::components::time_graph::Axis;\n\n    const Y_LABELS: [Cow<'static, str>; 3] = [\n        Cow::Borrowed(\"0%\"),\n        Cow::Borrowed(\"50%\"),\n        Cow::Borrowed(\"100%\"),\n    ];\n\n    fn create_time_graph() -> TimeGraph<'static> {\n        TimeGraph {\n            title: \" Network \".into(),\n            x_min: -15000.0,\n            hide_x_labels: false,\n            y_bounds: AxisBound::Max(100.5),\n            y_labels: &Y_LABELS,\n            graph_style: Style::default().fg(Color::Red),\n            border_style: Style::default().fg(Color::Blue),\n            border_type: BorderType::Plain,\n            is_selected: false,\n            is_expanded: false,\n            title_style: Style::default().fg(Color::Cyan),\n            legend_position: None,\n            legend_constraints: None,\n            marker: Marker::Braille,\n            scaling: ChartScaling::Linear,\n        }\n    }\n\n    #[test]\n    fn time_graph_gen_x_axis() {\n        let tg = create_time_graph();\n        let style = Style::default().fg(Color::Red);\n        let x_axis = tg.generate_x_axis();\n\n        let actual = Axis::default()\n            .bounds(AxisBound::Min(-15000.0))\n            .labels(vec![Span::styled(\"15s\", style), Span::styled(\"0s\", style)])\n            .style(style);\n        assert_eq!(x_axis.bounds, actual.bounds);\n        assert_eq!(x_axis.labels, actual.labels);\n        assert_eq!(x_axis.style, actual.style);\n    }\n\n    #[test]\n    fn time_graph_gen_y_axis() {\n        let tg = create_time_graph();\n        let style = Style::default().fg(Color::Red);\n        let y_axis = tg.generate_y_axis();\n\n        let actual = Axis::default()\n            .bounds(AxisBound::Max(100.5))\n            .labels(vec![\n                Span::styled(\"0%\", style),\n                Span::styled(\"50%\", style),\n                Span::styled(\"100%\", style),\n            ])\n            .style(style);\n\n        assert_eq!(y_axis.bounds, actual.bounds);\n        assert_eq!(y_axis.labels, actual.labels);\n        assert_eq!(y_axis.style, actual.style);\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/time_graph/variants/auto_y_axis.rs",
    "content": "//! A variant of a [`crate::canvas::components::time_graph::TimeGraph`] that\n//! automatically adjusts the y-axis based on the data provided.\n"
  },
  {
    "path": "src/canvas/components/time_graph/variants/percent.rs",
    "content": "//! A variant of a [`TimeGraph`] that expects data to be in a percentage format, from 0.0 to 100.0.\n\nuse std::borrow::Cow;\n\nuse tui::{layout::Constraint, symbols::Marker};\n\nuse crate::{\n    app::AppConfigFields,\n    canvas::components::time_graph::{\n        AxisBound, ChartScaling, LegendPosition, TimeGraph, variants::get_border_style,\n    },\n    options::config::style::Styles,\n};\n\n/// Acts as a wrapper for a [`TimeGraph`] that expects data to be in a percentage format,\npub(crate) struct PercentTimeGraph<'a> {\n    /// The total display range of the graph in milliseconds.\n    ///\n    /// TODO: Make this a [`std::time::Duration`].\n    pub(crate) display_range: u64,\n\n    /// Whether to hide the x-axis labels.\n    pub(crate) hide_x_labels: bool,\n\n    /// The app config fields.\n    ///\n    /// This is mostly used as a shared mutability workaround due to [`App`]\n    /// being a giant state struct.\n    pub(crate) app_config_fields: &'a AppConfigFields,\n\n    /// The current widget selected by the app.\n    ///\n    /// This is mostly used as a shared mutability workaround due to [`App`]\n    /// being a giant state struct.\n    pub(crate) current_widget: u64,\n\n    /// Whether the current widget is expanded.\n    ///  \n    /// This is mostly used as a shared mutability workaround due to [`App`]\n    /// being a giant state struct.\n    pub(crate) is_expanded: bool,\n\n    /// The title of the graph.\n    pub(crate) title: Cow<'a, str>,\n\n    /// A reference to the styles.\n    pub(crate) styles: &'a Styles,\n\n    /// The widget ID corresponding to this graph.\n    pub(crate) widget_id: u64,\n\n    /// The position of the legend.\n    pub(crate) legend_position: Option<LegendPosition>,\n\n    /// The constraints for the legend.\n    pub(crate) legend_constraints: Option<(Constraint, Constraint)>,\n}\n\nimpl<'a> PercentTimeGraph<'a> {\n    /// Return the final [`TimeGraph`].\n    pub fn build(self) -> TimeGraph<'a> {\n        const Y_BOUNDS: AxisBound = AxisBound::Max(100.5);\n        const Y_LABELS: [Cow<'static, str>; 2] = [Cow::Borrowed(\"  0%\"), Cow::Borrowed(\"100%\")];\n\n        let x_min = -(self.display_range as f64);\n\n        let marker = if self.app_config_fields.use_dot {\n            Marker::Dot\n        } else {\n            Marker::Braille\n        };\n\n        let graph_style = self.styles.graph_style;\n        let border_style = get_border_style(self.styles, self.widget_id, self.current_widget);\n        let title_style = self.styles.widget_title_style;\n        let border_type = self.styles.border_type;\n\n        TimeGraph {\n            x_min,\n            hide_x_labels: self.hide_x_labels,\n            y_bounds: Y_BOUNDS,\n            y_labels: &Y_LABELS,\n            graph_style,\n            border_style,\n            border_type,\n            title: self.title,\n            is_selected: self.current_widget == self.widget_id,\n            is_expanded: self.is_expanded,\n            title_style,\n            legend_position: self.legend_position,\n            legend_constraints: self.legend_constraints,\n            marker,\n            scaling: ChartScaling::Linear,\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/time_graph/variants.rs",
    "content": "use tui::style::Style;\n\nuse crate::options::config::style::Styles;\n\npub(crate) mod auto_y_axis;\npub(crate) mod percent;\n\nfn get_border_style(styles: &Styles, widget_id: u64, selected_widget_id: u64) -> Style {\n    let is_on_widget = widget_id == selected_widget_id;\n    if is_on_widget {\n        styles.highlighted_border_style\n    } else {\n        styles.border_style\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/time_graph/vendored/canvas.rs",
    "content": "//! Vendored from <https://github.com/fdehau/tui-rs/blob/fafad6c96109610825aad89c4bba5253e01101ed/src/widgets/canvas/mod.rs>\n//! and <https://github.com/ratatui/ratatui/blob/65c520245aa20e99e64d9ffcb2062a4502a699ea/ratatui-widgets/src/canvas.rs>.\n//!\n//! The main thing this is pulled in for is overriding how `BrailleGrid`'s draw\n//! logic works, as changing it is needed in order to draw all datasets in only\n//! one layer back in [`super::TimeChart::render`]. More specifically,\n//! the current implementation in ratatui `|=`s all the cells together if they\n//! overlap, but since we are smashing all the layers together which may have\n//! different colours, we instead just _replace_ whatever was in that cell\n//! with the newer colour + character.\n//!\n//! See <https://github.com/ClementTsang/bottom/pull/918> and <https://github.com/ClementTsang/bottom/pull/937> for the\n//! original motivation.\n\nuse ratatui_core::symbols::{\n    braille::BRAILLE,\n    pixel::{OCTANTS, QUADRANTS, SEXTANTS},\n};\nuse tui::{\n    buffer::Buffer,\n    layout::Rect,\n    prelude::BlockExt,\n    style::{Color, Style},\n    symbols::Marker,\n    text::Line,\n    widgets::{\n        Block, Widget,\n        canvas::{Line as CanvasLine, Points},\n    },\n};\n\nuse super::grid::{CharGrid, Grid, HalfBlockGrid, PatternGrid};\n\n/// Interface for all shapes that may be drawn on a Canvas widget.\npub trait Shape {\n    fn draw(&self, painter: &mut Painter<'_, '_>);\n}\n\nimpl Shape for CanvasLine {\n    fn draw(&self, painter: &mut Painter<'_, '_>) {\n        let (x1, y1) = match painter.get_point(self.x1, self.y1) {\n            Some(c) => c,\n            None => return,\n        };\n        let (x2, y2) = match painter.get_point(self.x2, self.y2) {\n            Some(c) => c,\n            None => return,\n        };\n        let (dx, x_range) = if x2 >= x1 {\n            (x2 - x1, x1..=x2)\n        } else {\n            (x1 - x2, x2..=x1)\n        };\n        let (dy, y_range) = if y2 >= y1 {\n            (y2 - y1, y1..=y2)\n        } else {\n            (y1 - y2, y2..=y1)\n        };\n\n        if dx == 0 {\n            for y in y_range {\n                painter.paint(x1, y, self.color);\n            }\n        } else if dy == 0 {\n            for x in x_range {\n                painter.paint(x, y1, self.color);\n            }\n        } else if dy < dx {\n            if x1 > x2 {\n                draw_line_low(painter, x2, y2, x1, y1, self.color);\n            } else {\n                draw_line_low(painter, x1, y1, x2, y2, self.color);\n            }\n        } else if y1 > y2 {\n            draw_line_high(painter, x2, y2, x1, y1, self.color);\n        } else {\n            draw_line_high(painter, x1, y1, x2, y2, self.color);\n        }\n    }\n}\n\nfn draw_line_low(\n    painter: &mut Painter<'_, '_>, x1: usize, y1: usize, x2: usize, y2: usize, color: Color,\n) {\n    let dx = (x2 - x1) as isize;\n    let dy = (y2 as isize - y1 as isize).abs();\n    let mut d = 2 * dy - dx;\n    let mut y = y1;\n    for x in x1..=x2 {\n        painter.paint(x, y, color);\n        if d > 0 {\n            y = if y1 > y2 {\n                y.saturating_sub(1)\n            } else {\n                y.saturating_add(1)\n            };\n            d -= 2 * dx;\n        }\n        d += 2 * dy;\n    }\n}\n\nfn draw_line_high(\n    painter: &mut Painter<'_, '_>, x1: usize, y1: usize, x2: usize, y2: usize, color: Color,\n) {\n    let dx = (x2 as isize - x1 as isize).abs();\n    let dy = (y2 - y1) as isize;\n    let mut d = 2 * dx - dy;\n    let mut x = x1;\n    for y in y1..=y2 {\n        painter.paint(x, y, color);\n        if d > 0 {\n            x = if x1 > x2 {\n                x.saturating_sub(1)\n            } else {\n                x.saturating_add(1)\n            };\n            d -= 2 * dy;\n        }\n        d += 2 * dx;\n    }\n}\n\nimpl Shape for Points<'_> {\n    fn draw(&self, painter: &mut Painter<'_, '_>) {\n        for (x, y) in self.coords {\n            if let Some((x, y)) = painter.get_point(*x, *y) {\n                painter.paint(x, y, self.color);\n            }\n        }\n    }\n}\n\n/// Label to draw some text on the canvas\n#[derive(Debug, Clone)]\npub struct Label<'a> {\n    x: f64,\n    y: f64,\n    spans: Line<'a>,\n}\n\n#[derive(Debug)]\npub struct Painter<'a, 'b> {\n    context: &'a mut Context<'b>,\n    resolution: (f64, f64),\n}\n\nimpl Painter<'_, '_> {\n    /// Convert the (x, y) coordinates to location of a point on the grid.\n    pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {\n        let [left, right] = self.context.x_bounds;\n        let [bottom, top] = self.context.y_bounds;\n        if x < left || x > right || y < bottom || y > top {\n            return None;\n        }\n        let width = right - left;\n        let height = top - bottom;\n        if width <= 0.0 || height <= 0.0 {\n            return None;\n        }\n        let x = ((x - left) * (self.resolution.0 - 1.0) / width).round() as usize;\n        let y = ((top - y) * (self.resolution.1 - 1.0) / height).round() as usize;\n        Some((x, y))\n    }\n\n    /// Paint a point of the grid.\n    pub fn paint(&mut self, x: usize, y: usize, color: Color) {\n        self.context.grid.paint(x, y, color);\n    }\n}\n\nimpl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {\n    fn from(context: &'a mut Context<'b>) -> Painter<'a, 'b> {\n        let resolution = context.grid.resolution();\n        Painter {\n            context,\n            resolution,\n        }\n    }\n}\n\n/// Holds the state of the Canvas when painting to it.\n#[derive(Debug)]\npub struct Context<'a> {\n    x_bounds: [f64; 2],\n    y_bounds: [f64; 2],\n    grid: Box<dyn Grid>,\n    dirty: bool,\n    labels: Vec<Label<'a>>,\n}\n\nimpl<'a> Context<'a> {\n    pub fn new(\n        width: u16, height: u16, x_bounds: [f64; 2], y_bounds: [f64; 2], marker: Marker,\n    ) -> Context<'a> {\n        let grid = Self::marker_to_grid(width, height, marker);\n\n        Context {\n            x_bounds,\n            y_bounds,\n            grid,\n            dirty: false,\n            labels: Vec::new(),\n        }\n    }\n\n    fn marker_to_grid(width: u16, height: u16, marker: Marker) -> Box<dyn Grid> {\n        match marker {\n            Marker::Dot => Box::new(CharGrid::new(width, height, '•')),\n            Marker::Block => Box::new(CharGrid::new(width, height, '█').apply_color_to_bg()),\n            Marker::Bar => Box::new(CharGrid::new(width, height, '▄')),\n            Marker::Braille => Box::new(PatternGrid::<2, 4>::new(width, height, &BRAILLE)),\n            Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),\n            Marker::Quadrant => Box::new(PatternGrid::<2, 2>::new(width, height, &QUADRANTS)),\n            Marker::Sextant => Box::new(PatternGrid::<2, 3>::new(width, height, &SEXTANTS)),\n            Marker::Octant => Box::new(PatternGrid::<2, 4>::new(width, height, &OCTANTS)),\n            _ => Box::new(PatternGrid::<2, 4>::new(width, height, &BRAILLE)), // Fall back to braille if not supported.\n        }\n    }\n\n    /// Draw any object that may implement the Shape trait\n    pub fn draw<S>(&mut self, shape: &S)\n    where\n        S: Shape,\n    {\n        self.dirty = true;\n        let mut painter = Painter::from(self);\n        shape.draw(&mut painter);\n    }\n}\n\n/// The Canvas widget may be used to draw more detailed figures using braille\n/// patterns (each cell can have a braille character in 8 different positions).\npub struct Canvas<'a, F>\nwhere\n    F: Fn(&mut Context<'_>),\n{\n    block: Option<Block<'a>>,\n    x_bounds: [f64; 2],\n    y_bounds: [f64; 2],\n    paint_func: Option<F>,\n    background_color: Color,\n    marker: Marker,\n}\n\nimpl<'a, F> Default for Canvas<'a, F>\nwhere\n    F: Fn(&mut Context<'_>),\n{\n    fn default() -> Canvas<'a, F> {\n        Canvas {\n            block: None,\n            x_bounds: [0.0, 0.0],\n            y_bounds: [0.0, 0.0],\n            paint_func: None,\n            background_color: Color::Reset,\n            marker: Marker::Braille,\n        }\n    }\n}\n\nimpl<'a, F> Canvas<'a, F>\nwhere\n    F: Fn(&mut Context<'_>),\n{\n    pub fn x_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {\n        self.x_bounds = bounds;\n        self\n    }\n\n    pub fn y_bounds(mut self, bounds: [f64; 2]) -> Canvas<'a, F> {\n        self.y_bounds = bounds;\n        self\n    }\n\n    /// Store the closure that will be used to draw to the Canvas\n    pub fn paint(mut self, f: F) -> Canvas<'a, F> {\n        self.paint_func = Some(f);\n        self\n    }\n\n    pub fn background_color(mut self, color: Color) -> Canvas<'a, F> {\n        self.background_color = color;\n        self\n    }\n\n    /// Change the type of points used to draw the shapes. By default, the\n    /// braille patterns are used as they provide a more fine-grained result,\n    /// but you might want to use the simple dot or block instead if the\n    /// targeted terminal does not support those symbols.\n    pub fn marker(mut self, marker: Marker) -> Canvas<'a, F> {\n        self.marker = marker;\n        self\n    }\n}\n\nimpl<F> Widget for Canvas<'_, F>\nwhere\n    F: Fn(&mut Context<'_>),\n{\n    fn render(self, area: Rect, buf: &mut Buffer) {\n        Widget::render(&self, area, buf);\n    }\n}\n\nimpl<F> Widget for &Canvas<'_, F>\nwhere\n    F: Fn(&mut Context<'_>),\n{\n    fn render(self, area: Rect, buf: &mut Buffer) {\n        self.block.as_ref().render(area, buf);\n        let canvas_area = self.block.inner_if_some(area);\n        if canvas_area.is_empty() {\n            return;\n        }\n\n        buf.set_style(canvas_area, Style::default().bg(self.background_color));\n\n        let width = canvas_area.width as usize;\n\n        let Some(ref painter) = self.paint_func else {\n            return;\n        };\n\n        // Create a blank context that match the size of the canvas\n        let mut ctx = Context::new(\n            canvas_area.width,\n            canvas_area.height,\n            self.x_bounds,\n            self.y_bounds,\n            self.marker,\n        );\n        // Paint to this context\n        painter(&mut ctx);\n        // ctx.finish(); // Not needed, we have no layers.\n\n        // Instead, paint whatever is in the ctx.\n        let layer = ctx.grid.save();\n\n        for (index, layer_cell) in layer.contents.iter().enumerate() {\n            let (x, y) = (\n                (index % width) as u16 + canvas_area.left(),\n                (index / width) as u16 + canvas_area.top(),\n            );\n\n            if let Some(cell) = buf.cell_mut((x, y)) {\n                if let Some(symbol) = layer_cell.symbol {\n                    cell.set_char(symbol);\n                }\n                if let Some(fg) = layer_cell.fg {\n                    cell.set_fg(fg);\n                }\n                if let Some(bg) = layer_cell.bg {\n                    cell.set_bg(bg);\n                }\n            }\n        }\n\n        // Reset the grid and mark as non-dirty.\n        ctx.grid.reset();\n        ctx.dirty = false;\n\n        // Finally draw the labels\n        let left = self.x_bounds[0];\n        let right = self.x_bounds[1];\n        let top = self.y_bounds[1];\n        let bottom = self.y_bounds[0];\n        let width = (self.x_bounds[1] - self.x_bounds[0]).abs();\n        let height = (self.y_bounds[1] - self.y_bounds[0]).abs();\n        let resolution = {\n            let width = f64::from(canvas_area.width - 1);\n            let height = f64::from(canvas_area.height - 1);\n            (width, height)\n        };\n        for label in ctx\n            .labels\n            .iter()\n            .filter(|l| l.x >= left && l.x <= right && l.y <= top && l.y >= bottom)\n        {\n            let x = ((label.x - left) * resolution.0 / width) as u16 + canvas_area.left();\n            let y = ((top - label.y) * resolution.1 / height) as u16 + canvas_area.top();\n            buf.set_line(x, y, &label.spans, canvas_area.right() - x);\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/time_graph/vendored/grid.rs",
    "content": "//! Vendored [starting from here](https://github.com/ratatui/ratatui/blob/65c520245aa20e99e64d9ffcb2062a4502a699ea/ratatui-widgets/src/canvas.rs#L322).\n\nuse std::{fmt::Debug, iter::zip};\n\nuse itertools::Itertools;\nuse tui::{style::Color, symbols};\n\n/// A single layer of the canvas.\n///\n/// This allows the canvas to be drawn in multiple layers. This is useful if you want to draw\n/// multiple shapes on the canvas in specific order.\n///\n/// **NOTE**: In the vendored version, we don't ever actually want to do this.\n#[derive(Debug)]\npub(super) struct Layer {\n    pub(super) contents: Vec<LayerCell>,\n}\n\n/// A cell within a layer.\n///\n/// If a [`Context`] contains multiple layers, then the symbol, foreground, and background colors\n/// for a character will be determined by the top-most layer that provides a value for that\n/// character. For example, a chart drawn with [`Marker::Block`] may provide the background color,\n/// and a later chart drawn with [`Marker::Braille`] may provide the symbol and foreground color.\n#[derive(Debug)]\npub(super) struct LayerCell {\n    pub(super) symbol: Option<char>,\n    pub(super) fg: Option<Color>,\n    pub(super) bg: Option<Color>,\n}\n\n/// A grid of cells that can be painted on.\n///\n/// The grid represents a particular screen region measured in rows and columns. The underlying\n/// resolution of the grid might exceed the number of rows and columns. For example, a grid of\n/// Braille patterns will have a resolution of 2x4 dots per cell. This means that a grid of 10x10\n/// cells will have a resolution of 20x40 dots.\npub(super) trait Grid: Debug {\n    /// Get the resolution of the grid in number of dots.\n    ///\n    /// This doesn't have to be the same as the number of rows and columns of the grid. For example,\n    /// a grid of Braille patterns will have a resolution of 2x4 dots per cell. This means that a\n    /// grid of 10x10 cells will have a resolution of 20x40 dots.\n    fn resolution(&self) -> (f64, f64);\n    /// Paint a point of the grid.\n    ///\n    /// The point is expressed in number of dots starting at the origin of the grid in the top left\n    /// corner. Note that this is not the same as the `(x, y)` coordinates of the canvas.\n    fn paint(&mut self, x: usize, y: usize, color: Color);\n    /// Save the current state of the [`Grid`] as a layer to be rendered\n    fn save(&self) -> Layer;\n    /// Reset the grid to its initial state\n    fn reset(&mut self);\n}\n\n/// The pattern and colour of a `PatternGrid` cell.\n#[derive(Copy, Clone, Debug, Default)]\nstruct PatternCell {\n    /// The pattern of a grid character.\n    ///\n    /// The pattern is stored in the lower bits in a row-major order. For instance, for a 2x4\n    /// pattern marker, bits 0 to 7 of this field should represent the following pseudo-pixels:\n    ///\n    /// | 0 1 |\n    /// | 2 3 |\n    /// | 4 5 |\n    /// | 6 7 |\n    pattern: u8,\n    /// The color of a cell only supports foreground colors for now as there's no way to\n    /// individually set the background color of each pseudo-pixel in a pattern character.\n    color: Option<Color>,\n}\n\n/// The `PatternGrid` is a grid made up of cells each containing a `W`x`H` pattern character.\n///\n/// This makes it possible to draw shapes with a resolution of e.g. 2x4 (Braille or Unicode octant)\n/// per cell.\n/// Font support for the relevant pattern character is required. If your terminal or font does not\n/// support the relevant Unicode block, you will see Unicode replacement characters (�) instead.\n///\n/// This grid type only supports a single foreground colour for each `W`x`H` pattern character.\n/// There is no way to set the individual colour of each pseudo-pixel.\n#[derive(Debug)]\npub(super) struct PatternGrid<const W: usize, const H: usize> {\n    /// Width of the grid in number of terminal columns\n    width: u16,\n    /// Height of the grid in number of terminal rows\n    height: u16,\n    /// Pattern and color of the cells.\n    cells: Vec<PatternCell>,\n    /// Lookup table mapping patterns to characters.\n    char_table: &'static [char],\n}\n\nimpl<const W: usize, const H: usize> PatternGrid<W, H> {\n    /// Statically check that the dimension of the pattern is supported.\n    const _PATTERN_DIMENSION_CHECK: usize = u8::BITS as usize - W * H;\n\n    /// Create a new `PatternGrid` with the given width and height measured in terminal columns\n    /// and rows respectively.\n    pub(super) fn new(width: u16, height: u16, char_table: &'static [char]) -> Self {\n        // Cause a static error if the pattern doesn't fit within 8 bits.\n        let _ = Self::_PATTERN_DIMENSION_CHECK;\n\n        let length = usize::from(width) * usize::from(height);\n        Self {\n            width,\n            height,\n            cells: vec![PatternCell::default(); length],\n            char_table,\n        }\n    }\n}\n\nimpl<const W: usize, const H: usize> Grid for PatternGrid<W, H> {\n    fn resolution(&self) -> (f64, f64) {\n        (\n            f64::from(self.width) * W as f64,\n            f64::from(self.height) * H as f64,\n        )\n    }\n\n    fn paint(&mut self, x: usize, y: usize, color: Color) {\n        let index = y\n            .saturating_div(H)\n            .saturating_mul(self.width as usize)\n            .saturating_add(x.saturating_div(W));\n\n        // The ratatui/tui-rs implementation; this gives a more merged\n        // look, but it also makes it a bit harder to read in some cases.\n        //\n        // using get_mut here because we are indexing the vector with usize values\n        // and we want to make sure we don't panic if the index is out of bounds\n        // if let Some(cell) = self.cells.get_mut(index) {\n        //     cell.pattern |= 1u8 << ((x % W) + W * (y % H));\n        //     cell.color = Some(color);\n        // }\n\n        // Custom implementation do distinguish between lines better.\n        if let Some(cell) = self.cells.get_mut(index) {\n            if let Some(curr_color) = &mut cell.color {\n                if *curr_color != color {\n                    // If the colour doesn't match, then reset the colour and cell.\n                    *curr_color = color;\n                    cell.pattern = 1u8 << ((x % W) + W * (y % H));\n                } else {\n                    // If it does match, then combine it with the previous underlying cell.\n                    cell.pattern |= 1u8 << ((x % W) + W * (y % H));\n                }\n            } else {\n                // If there's no color then just assume it's a brand new cell.\n                cell.color = Some(color);\n                cell.pattern = 1u8 << ((x % W) + W * (y % H));\n            }\n        }\n    }\n\n    fn save(&self) -> Layer {\n        let contents = self\n            .cells\n            .iter()\n            .map(|&cell| {\n                let symbol = match cell.pattern {\n                    // Skip rendering blank patterns to allow layers underneath\n                    // to show through.\n                    0 => None,\n                    idx => Some(self.char_table[idx as usize]),\n                };\n\n                LayerCell {\n                    symbol,\n                    fg: cell.color,\n                    // Patterns only affect foreground.\n                    bg: None,\n                }\n            })\n            .collect();\n\n        Layer { contents }\n    }\n\n    fn reset(&mut self) {\n        self.cells.fill_with(Default::default);\n    }\n}\n\n// impl Grid for BrailleGrid {\n//     fn resolution(&self) -> (f64, f64) {\n//         (f64::from(self.width) * 2.0, f64::from(self.height) * 4.0)\n//     }\n//\n//     fn paint(&mut self, x: usize, y: usize, color: Color) {\n//         // Note the braille array corresponds to:\n//         // ```\n//         // ⠁⠈\n//         // ⠂⠐\n//         // ⠄⠠\n//         // ⡀⢀\n//         // ```\n//         const BLANK: u16 = 0x2800;\n//         const DOTS: [[u16; 2]; 4] = [\n//             [0x0001, 0x0008],\n//             [0x0002, 0x0010],\n//             [0x0004, 0x0020],\n//             [0x0040, 0x0080],\n//         ];\n//\n//         let index = y / 4 * self.width as usize + x / 2;\n//\n//         // The ratatui/tui-rs implementation; this gives a more merged\n//         // look, but it also makes it a bit harder to read in some cases.\n//\n//         // if let Some(c) = self.utf16_code_points.get_mut(index) {\n//         //     *c |= DOTS[y % 4][x % 2];\n//         // }\n//         // if let Some(c) = self.colors.get_mut(index) {\n//         //     *c = color;\n//         // }\n//\n//         // Custom implementation to distinguish between lines better.\n//         if let Some(curr_color) = self.colors.get_mut(index) {\n//             if *curr_color != color {\n//                 *curr_color = color;\n//                 if let Some(cell) = self.utf16_code_points.get_mut(index) {\n//                     *cell = BLANK | DOTS[y % 4][x % 2];\n//                 }\n//             } else if let Some(cell) = self.utf16_code_points.get_mut(index) {\n//                 *cell |= DOTS[y % 4][x % 2];\n//             }\n//         }\n//     }\n//\n//     fn save(&self) -> Layer {\n//         let string = String::from_utf16(&self.utf16_code_points).expect(\"valid UTF-16 data\");\n//         // the background color is always reset for braille patterns\n//         let colors = self.colors.iter().map(|c| (*c, Color::Reset)).collect();\n//         Layer { string, colors }\n//     }\n//\n//     fn reset(&mut self) {\n//         self.utf16_code_points.fill(BLANK);\n//         self.colors.fill(Color::Reset);\n//     }\n// }\n\n/// The `CharGrid` is a grid made up of cells each containing a single character.\n///\n/// This makes it possible to draw shapes with a resolution of 1x1 dots per cell. This is useful\n/// when you want to draw shapes with a low resolution.\n#[derive(Debug)]\npub(super) struct CharGrid {\n    /// Width of the grid in number of terminal columns\n    width: u16,\n    /// Height of the grid in number of terminal rows\n    height: u16,\n    /// The color of each cell\n    cells: Vec<Option<Color>>,\n\n    /// The character to use for every cell - e.g. a block, dot, etc.\n    cell_char: char,\n\n    /// If true, apply the color to the background as well as the foreground. This is used for\n    /// [`Marker::Block`], so that it will overwrite any previous foreground character, but also\n    /// leave a background that can be overlaid with an additional foreground character.\n    apply_color_to_bg: bool,\n}\n\nimpl CharGrid {\n    /// Create a new `CharGrid` with the given width and height measured in terminal columns and\n    /// rows respectively.\n    pub(super) fn new(width: u16, height: u16, cell_char: char) -> Self {\n        let length = usize::from(width) * usize::from(height);\n        Self {\n            width,\n            height,\n            cells: vec![None; length],\n            cell_char,\n            apply_color_to_bg: false,\n        }\n    }\n\n    pub(super) fn apply_color_to_bg(self) -> Self {\n        Self {\n            apply_color_to_bg: true,\n            ..self\n        }\n    }\n}\n\nimpl Grid for CharGrid {\n    fn resolution(&self) -> (f64, f64) {\n        (f64::from(self.width), f64::from(self.height))\n    }\n\n    fn paint(&mut self, x: usize, y: usize, color: Color) {\n        let index = y.saturating_mul(self.width as usize).saturating_add(x);\n        // using get_mut here because we are indexing the vector with usize values\n        // and we want to make sure we don't panic if the index is out of bounds\n        if let Some(c) = self.cells.get_mut(index) {\n            *c = Some(color);\n        }\n    }\n\n    fn save(&self) -> Layer {\n        Layer {\n            contents: self\n                .cells\n                .iter()\n                .map(|&color| LayerCell {\n                    symbol: color.map(|_| self.cell_char),\n                    fg: color,\n                    bg: color.filter(|_| self.apply_color_to_bg),\n                })\n                .collect(),\n        }\n    }\n\n    fn reset(&mut self) {\n        self.cells.fill(None);\n    }\n}\n\n/// The `HalfBlockGrid` is a grid made up of cells each containing a half block character.\n///\n/// In terminals, each character is usually twice as tall as it is wide. Unicode has a couple of\n/// vertical half block characters, the upper half block '▀' and lower half block '▄' which take up\n/// half the height of a normal character but the full width. Together with an empty space ' ' and a\n/// full block '█', we can effectively double the resolution of a single cell. In addition, because\n/// each character can have a foreground and background color, we can control the color of the upper\n/// and lower half of each cell. This allows us to draw shapes with a resolution of 1x2 \"pixels\" per\n/// cell.\n///\n/// This allows for more flexibility than the `PatternGrid` which only supports a single\n/// foreground color for each 2x4 dots cell, and the `CharGrid` which only supports a single\n/// character for each cell.\n#[derive(Debug)]\n\npub(super) struct HalfBlockGrid {\n    /// Width of the grid in number of terminal columns\n    width: u16,\n    /// Height of the grid in number of terminal rows\n    height: u16,\n    /// Represents a single color for each \"pixel\" arranged in column, row order\n    pixels: Vec<Vec<Option<Color>>>,\n}\n\nimpl HalfBlockGrid {\n    /// Create a new `HalfBlockGrid` with the given width and height measured in terminal columns\n    /// and rows respectively.\n    pub(super) fn new(width: u16, height: u16) -> Self {\n        Self {\n            width,\n            height,\n            pixels: vec![vec![None; width as usize]; (height as usize) * 2],\n        }\n    }\n}\n\nimpl Grid for HalfBlockGrid {\n    fn resolution(&self) -> (f64, f64) {\n        (f64::from(self.width), f64::from(self.height) * 2.0)\n    }\n\n    fn paint(&mut self, x: usize, y: usize, color: Color) {\n        self.pixels[y][x] = Some(color);\n    }\n\n    fn save(&self) -> Layer {\n        // Given that we store the pixels in a grid, and that we want to use 2 pixels arranged\n        // vertically to form a single terminal cell, which can be either empty, upper half block,\n        // lower half block or full block, we need examine the pixels in vertical pairs to decide\n        // what character to print in each cell. So these are the 4 states we use to represent each\n        // cell:\n        //\n        // 1. upper: reset, lower: reset => ' ' fg: reset / bg: reset\n        // 2. upper: reset, lower: color => '▄' fg: lower color / bg: reset\n        // 3. upper: color, lower: reset => '▀' fg: upper color / bg: reset\n        // 4. upper: color, lower: color => '▀' fg: upper color / bg: lower color\n        //\n        // Note that because the foreground reset color (i.e. default foreground color) is usually\n        // not the same as the background reset color (i.e. default background color), we need to\n        // swap around the colors for that state (2 reset/color).\n        //\n        // When the upper and lower colors are the same, we could continue to use an upper half\n        // block, but we choose to use a full block instead. This allows us to write unit tests that\n        // treat the cell as a single character instead of two half block characters.\n\n        // first we join each adjacent row together to get an iterator that contains vertical pairs\n        // of pixels, with the lower row being the first element in the pair\n        let vertical_color_pairs = self\n            .pixels\n            .iter()\n            .tuples()\n            .flat_map(|(upper_row, lower_row)| zip(upper_row, lower_row));\n\n        // Then we determine the character to print for each pair, along with the color of the\n        // foreground and background.\n        let contents = vertical_color_pairs\n            .map(|(upper, lower)| {\n                let (symbol, fg, bg) = match (upper, lower) {\n                    (None, None) => (None, None, None),\n                    (None, Some(lower)) => (Some(symbols::half_block::LOWER), Some(*lower), None),\n                    (Some(upper), None) => (Some(symbols::half_block::UPPER), Some(*upper), None),\n                    (Some(upper), Some(lower)) if lower == upper => {\n                        (Some(symbols::half_block::FULL), Some(*upper), Some(*lower))\n                    }\n                    (Some(upper), Some(lower)) => {\n                        (Some(symbols::half_block::UPPER), Some(*upper), Some(*lower))\n                    }\n                };\n                LayerCell { symbol, fg, bg }\n            })\n            .collect();\n\n        Layer { contents }\n    }\n\n    fn reset(&mut self) {\n        self.pixels.fill(vec![None; self.width as usize]);\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/time_graph/vendored/points.rs",
    "content": "use itertools::Itertools;\nuse tui::{\n    style::Color,\n    widgets::{\n        GraphType,\n        canvas::{Line as CanvasLine, Points},\n    },\n};\n\nuse super::{Context, Data, Point, TimeChart};\n\nimpl TimeChart<'_> {\n    pub(crate) fn draw_points(&self, ctx: &mut Context<'_>) {\n        // Idea is to:\n        // - Go over all datasets, determine *where* a point will be drawn.\n        // - Last point wins for what gets drawn.\n        // - We set _all_ points for all datasets before actually rendering.\n        //\n        // By doing this, it's a bit more efficient from my experience than looping\n        // over each dataset and rendering a new layer each time.\n        //\n        // See https://github.com/ClementTsang/bottom/pull/918 and\n        // https://github.com/ClementTsang/bottom/pull/937 for the original motivation.\n        //\n        // We also additionally do some interpolation logic because we may get caught\n        // missing some points when drawing, but we generally want to avoid\n        // jarring gaps between the edges when there's a point that is off\n        // screen and so a line isn't drawn (right edge generally won't have this issue\n        // issue but it can happen in some cases).\n\n        for dataset in &self.datasets {\n            let Data::Some { times, values } = dataset.data else {\n                continue;\n            };\n\n            let Some(current_time) = times.last() else {\n                continue;\n            };\n\n            let color = dataset.style.fg.unwrap_or(Color::Reset);\n            let left_edge = self.x_axis.bounds.get_bounds()[0];\n\n            // TODO: (points_rework_v1) Can we instead modify the range so it's based on the epoch rather than having to convert?\n            // TODO: (points_rework_v1) Is this efficient? Or should I prune using take_while first?\n            for (curr, next) in values\n                .iter_along_base(times)\n                .rev()\n                .map(|(&time, &val)| {\n                    let from_start = -(current_time.duration_since(time).as_millis() as f64);\n\n                    // XXX: Should this be generic over dataset.graph_type instead? That would allow us to move\n                    // transformations behind a type - however, that also means that there's some complexity added.\n                    (from_start, self.scaling.scale(val))\n                })\n                .tuple_windows()\n            {\n                if curr.0 == left_edge {\n                    // The current point hits the left edge. Draw just the current point and halt.\n                    ctx.draw(&Points {\n                        coords: &[curr],\n                        color,\n                    });\n\n                    break;\n                } else if next.0 < left_edge {\n                    // The next point goes past the left edge. Interpolate a point + the line and halt.\n                    let interpolated = interpolate_point(&next, &curr, left_edge);\n\n                    ctx.draw(&CanvasLine {\n                        x1: curr.0,\n                        y1: curr.1,\n                        x2: left_edge,\n                        y2: interpolated,\n                        color,\n                    });\n\n                    break;\n                } else {\n                    // Draw the current point and the line to the next point.\n                    if let GraphType::Line = dataset.graph_type {\n                        ctx.draw(&CanvasLine {\n                            x1: curr.0,\n                            y1: curr.1,\n                            x2: next.0,\n                            y2: next.1,\n                            color,\n                        });\n                    } else {\n                        ctx.draw(&Points {\n                            coords: &[curr],\n                            color,\n                        });\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Returns the y-axis value for a given `x`, given two points to draw a line\n/// between.\nfn interpolate_point(older_point: &Point, newer_point: &Point, x: f64) -> f64 {\n    let delta_x = newer_point.0 - older_point.0;\n    let delta_y = newer_point.1 - older_point.1;\n    let slope = delta_y / delta_x;\n\n    (older_point.1 + (x - older_point.0) * slope).max(0.0)\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn time_graph_test_interpolation() {\n        let data = [(-3.0, 8.0), (-1.0, 6.0), (0.0, 5.0)];\n\n        assert_eq!(interpolate_point(&data[1], &data[2], 0.0), 5.0);\n        assert_eq!(interpolate_point(&data[1], &data[2], -0.25), 5.25);\n        assert_eq!(interpolate_point(&data[1], &data[2], -0.5), 5.5);\n        assert_eq!(interpolate_point(&data[0], &data[1], -1.0), 6.0);\n        assert_eq!(interpolate_point(&data[0], &data[1], -1.5), 6.5);\n        assert_eq!(interpolate_point(&data[0], &data[1], -2.0), 7.0);\n        assert_eq!(interpolate_point(&data[0], &data[1], -2.5), 7.5);\n        assert_eq!(interpolate_point(&data[0], &data[1], -3.0), 8.0);\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/time_graph/vendored.rs",
    "content": "//! A [`tui::widgets::Chart`] but slightly more specialized to show\n//! right-aligned timeseries data.\n//!\n//! Generally should be updated to be in sync with [`chart.rs`](https://github.com/ratatui-org/ratatui/blob/main/src/widgets/chart.rs);\n//! the specializations are factored out to `time_graph/points.rs`.\n\nmod canvas;\nmod grid;\nmod points;\n\nuse std::{cmp::max, str::FromStr, time::Instant};\n\nuse canvas::*;\nuse tui::{\n    buffer::Buffer,\n    layout::{Alignment, Constraint, Flex, Layout, Rect},\n    style::{Color, Style, Styled},\n    symbols::{self, Marker},\n    text::{Line, Span},\n    widgets::{Block, BlockExt, Borders, GraphType, Widget},\n};\nuse unicode_width::UnicodeWidthStr;\n\nuse crate::{\n    app::data::Values,\n    utils::general::{saturating_log2, saturating_log10},\n};\n\npub const DEFAULT_LEGEND_CONSTRAINTS: (Constraint, Constraint) =\n    (Constraint::Ratio(1, 4), Constraint::Length(4));\n\n/// A single graph point.\npub type Point = (f64, f64);\n\n/// An axis bound type. Allows us to save a f64 since we know that we are\n/// usually bound from some values [0.0, a], or [-b, 0.0].\n#[derive(Debug, Default, Clone, Copy, PartialEq)]\npub enum AxisBound {\n    /// Just 0.\n    #[default]\n    Zero,\n    /// Bound by a minimum value to 0.\n    Min(f64),\n    /// Bound by 0 and a max value.\n    Max(f64),\n}\n\nimpl AxisBound {\n    fn get_bounds(&self) -> [f64; 2] {\n        match self {\n            AxisBound::Zero => [0.0, 0.0],\n            AxisBound::Min(min) => [*min, 0.0],\n            AxisBound::Max(max) => [0.0, *max],\n        }\n    }\n}\n\n/// An X or Y axis for the [`TimeChart`] widget\n#[derive(Debug, Default, Clone, PartialEq)]\npub struct Axis<'a> {\n    /// Title displayed next to axis end\n    pub(crate) title: Option<Line<'a>>,\n    /// Bounds for the axis (all data points outside these limits will not be\n    /// represented)\n    pub(crate) bounds: AxisBound,\n    /// A list of labels to put to the left or below the axis\n    pub(crate) labels: Option<Vec<Span<'a>>>,\n    /// The style used to draw the axis itself\n    pub(crate) style: Style,\n    /// The alignment of the labels of the Axis\n    pub(crate) labels_alignment: Alignment,\n}\n\nimpl<'a> Axis<'a> {\n    /// Sets the axis title\n    ///\n    /// It will be displayed at the end of the axis. For an X axis this is the\n    /// right, for a Y axis, this is the top.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    #[cfg_attr(not(test), expect(dead_code))]\n    pub fn title<T>(mut self, title: T) -> Axis<'a>\n    where\n        T: Into<Line<'a>>,\n    {\n        self.title = Some(title.into());\n        self\n    }\n\n    /// Sets the bounds of this axis.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn bounds(mut self, bounds: AxisBound) -> Axis<'a> {\n        self.bounds = bounds;\n        self\n    }\n\n    /// Sets the axis labels\n    ///\n    /// - For the X axis, the labels are displayed left to right.\n    /// - For the Y axis, the labels are displayed bottom to top.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn labels(mut self, labels: Vec<Span<'a>>) -> Axis<'a> {\n        self.labels = Some(labels);\n        self\n    }\n\n    /// Sets the axis style.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn style<S: Into<Style>>(mut self, style: S) -> Axis<'a> {\n        self.style = style.into();\n        self\n    }\n\n    /// Sets the labels alignment of the axis\n    ///\n    /// The alignment behaves differently based on the axis:\n    /// - Y axis: The labels are aligned within the area on the left of the axis\n    /// - X axis: The first X-axis label is aligned relative to the Y-axis\n    ///\n    /// On the X axis, this parameter only affects the first label.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    #[expect(dead_code)]\n    pub fn labels_alignment(mut self, alignment: Alignment) -> Axis<'a> {\n        self.labels_alignment = alignment;\n        self\n    }\n}\n\n/// Allow users to specify the position of a legend in a [`TimeChart`]\n///\n/// See [`TimeChart::legend_position`]\n#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]\npub enum LegendPosition {\n    /// Legend is centered on top\n    Top,\n    /// Legend is in the top-right corner. This is the **default**.\n    #[default]\n    TopRight,\n    /// Legend is in the top-left corner\n    TopLeft,\n    /// Legend is centered on the left\n    Left,\n    /// Legend is centered on the right\n    Right,\n    /// Legend is centered on the bottom\n    Bottom,\n    /// Legend is in the bottom-right corner\n    BottomRight,\n    /// Legend is in the bottom-left corner\n    BottomLeft,\n}\n\nimpl LegendPosition {\n    fn layout(\n        &self, area: Rect, legend_width: u16, legend_height: u16, x_title_width: u16,\n        y_title_width: u16,\n    ) -> Option<Rect> {\n        let mut height_margin = (area.height - legend_height) as i32;\n        if x_title_width != 0 {\n            height_margin -= 1;\n        }\n        if y_title_width != 0 {\n            height_margin -= 1;\n        }\n        if height_margin < 0 {\n            return None;\n        };\n\n        let (x, y) = match self {\n            Self::TopRight => {\n                if legend_width + y_title_width > area.width {\n                    (area.right() - legend_width, area.top() + 1)\n                } else {\n                    (area.right() - legend_width, area.top())\n                }\n            }\n            Self::TopLeft => {\n                if y_title_width != 0 {\n                    (area.left(), area.top() + 1)\n                } else {\n                    (area.left(), area.top())\n                }\n            }\n            Self::Top => {\n                let x = (area.width - legend_width) / 2;\n                if area.left() + y_title_width > x {\n                    (area.left() + x, area.top() + 1)\n                } else {\n                    (area.left() + x, area.top())\n                }\n            }\n            Self::Left => {\n                let mut y = (area.height - legend_height) / 2;\n                if y_title_width != 0 {\n                    y += 1;\n                }\n                if x_title_width != 0 {\n                    y = y.saturating_sub(1);\n                }\n                (area.left(), area.top() + y)\n            }\n            Self::Right => {\n                let mut y = (area.height - legend_height) / 2;\n                if y_title_width != 0 {\n                    y += 1;\n                }\n                if x_title_width != 0 {\n                    y = y.saturating_sub(1);\n                }\n                (area.right() - legend_width, area.top() + y)\n            }\n            Self::BottomLeft => {\n                if x_title_width + legend_width > area.width {\n                    (area.left(), area.bottom() - legend_height - 1)\n                } else {\n                    (area.left(), area.bottom() - legend_height)\n                }\n            }\n            Self::BottomRight => {\n                if x_title_width != 0 {\n                    (\n                        area.right() - legend_width,\n                        area.bottom() - legend_height - 1,\n                    )\n                } else {\n                    (area.right() - legend_width, area.bottom() - legend_height)\n                }\n            }\n            Self::Bottom => {\n                let x = area.left() + (area.width - legend_width) / 2;\n                if x + legend_width > area.right() - x_title_width {\n                    (x, area.bottom() - legend_height - 1)\n                } else {\n                    (x, area.bottom() - legend_height)\n                }\n            }\n        };\n\n        Some(Rect::new(x, y, legend_width, legend_height))\n    }\n}\n\n#[derive(Debug, PartialEq)]\npub struct ParseLegendPositionError;\n\nimpl FromStr for LegendPosition {\n    type Err = ParseLegendPositionError;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        match s {\n            \"top\" => Ok(Self::Top),\n            \"top-left\" => Ok(Self::TopLeft),\n            \"top-right\" => Ok(Self::TopRight),\n            \"left\" => Ok(Self::Left),\n            \"right\" => Ok(Self::Right),\n            \"bottom-left\" => Ok(Self::BottomLeft),\n            \"bottom\" => Ok(Self::Bottom),\n            \"bottom-right\" => Ok(Self::BottomRight),\n            _ => Err(ParseLegendPositionError),\n        }\n    }\n}\n\n#[derive(Debug, Default, Clone)]\nenum Data<'a> {\n    Some {\n        times: &'a [Instant],\n        values: &'a Values,\n    },\n    #[default]\n    None,\n}\n\n/// A group of data points\n///\n/// This is the main element composing a [`TimeChart`].\n///\n/// A dataset can be [named](Dataset::name). Only named datasets will be\n/// rendered in the legend.\n#[derive(Debug, Default, Clone)]\npub struct Dataset<'a> {\n    /// Name of the dataset (used in the legend if shown)\n    name: Option<Line<'a>>,\n    /// A reference to data.\n    data: Data<'a>,\n    /// Symbol used for each points of this dataset\n    marker: symbols::Marker,\n    /// Determines graph type used for drawing points\n    graph_type: GraphType,\n    /// Style used to plot this dataset\n    style: Style,\n}\n\nimpl<'a> Dataset<'a> {\n    /// Sets the name of the dataset.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn name<S>(mut self, name: S) -> Dataset<'a>\n    where\n        S: Into<Line<'a>>,\n    {\n        self.name = Some(name.into());\n        self\n    }\n\n    /// Sets the data points of this dataset\n    ///\n    /// Points will then either be rendered as scattered points or with lines\n    /// between them depending on [`Dataset::graph_type`].\n    ///\n    /// Data consist in an array of `f64` tuples (`(f64, f64)`), the first\n    /// element being X and the second Y. It's also worth noting that,\n    /// unlike the [`Rect`], here the Y axis is bottom to top, as in math.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn data(mut self, times: &'a [Instant], values: &'a Values) -> Dataset<'a> {\n        self.data = Data::Some { times, values };\n        self\n    }\n\n    /// Sets the kind of character to use to display this dataset\n    ///\n    /// You can use dots (`•`), blocks (`█`), bars (`▄`), braille (`⠓`, `⣇`,\n    /// `⣿`) or half-blocks (`█`, `▄`, and `▀`). See [symbols::Marker] for\n    /// more details.\n    ///\n    /// Note [`Marker::Braille`] requires a font that supports Unicode Braille\n    /// Patterns.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    #[expect(dead_code)]\n    pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {\n        self.marker = marker;\n        self\n    }\n\n    /// Sets how the dataset should be drawn\n    ///\n    /// [`TimeChart`] can draw either a [scatter](GraphType::Scatter) or\n    /// [line](GraphType::Line) charts. A scatter will draw only the points\n    /// in the dataset while a line will also draw a line between them. See\n    /// [`GraphType`] for more details\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {\n        self.graph_type = graph_type;\n        self\n    }\n\n    /// Sets the style of this dataset\n    ///\n    /// The given style will be used to draw the legend and the data points.\n    /// Currently the legend will use the entire style whereas the data\n    /// points will only use the foreground.\n    ///\n    /// `style` accepts any type that is convertible to [`Style`] (e.g.\n    /// [`Style`], [`Color`], or your own type that implements\n    /// [`Into<Style>`]).\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn style<S: Into<Style>>(mut self, style: S) -> Dataset<'a> {\n        self.style = style.into();\n        self\n    }\n}\n\n/// A container that holds all the infos about where to display each elements of\n/// the chart (axis, labels, legend, ...).\n#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]\nstruct ChartLayout {\n    /// Location of the title of the x axis\n    title_x: Option<(u16, u16)>,\n    /// Location of the title of the y axis\n    title_y: Option<(u16, u16)>,\n    /// Location of the first label of the x axis\n    label_x: Option<u16>,\n    /// Location of the first label of the y axis\n    label_y: Option<u16>,\n    /// Y coordinate of the horizontal axis\n    axis_x: Option<u16>,\n    /// X coordinate of the vertical axis\n    axis_y: Option<u16>,\n    /// Area of the legend\n    legend_area: Option<Rect>,\n    /// Area of the graph\n    graph_area: Rect,\n}\n\n/// Whether to additionally scale all values before displaying them. Defaults to none.\n#[derive(Default, Debug, Clone, Copy)]\npub(crate) enum ChartScaling {\n    #[default]\n    Linear,\n    Log10,\n    Log2,\n}\n\nimpl ChartScaling {\n    /// Scale a value.\n    pub(super) fn scale(&self, value: f64) -> f64 {\n        // Remember to do saturating log checks as otherwise 0.0 becomes inf, and you get\n        // gaps!\n        match self {\n            ChartScaling::Linear => value,\n            ChartScaling::Log10 => saturating_log10(value),\n            ChartScaling::Log2 => saturating_log2(value),\n        }\n    }\n}\n\n/// A \"custom\" chart, just a slightly tweaked [`tui::widgets::Chart`] from\n/// ratatui, but with greater control over the legend, and built with the idea\n/// of drawing data points relative to a time-based x-axis.\n///\n/// Main changes:\n/// - Styling option for the legend box\n/// - Automatically trimming out redundant draws in the x-bounds.\n/// - Automatic interpolation to points that fall *just* outside of the screen.\n#[derive(Debug, Default, Clone)]\npub(super) struct TimeChart<'a> {\n    /// A block to display around the widget eventually\n    block: Option<Block<'a>>,\n    /// The horizontal axis\n    x_axis: Axis<'a>,\n    /// The vertical axis\n    y_axis: Axis<'a>,\n    /// A reference to the datasets\n    datasets: Vec<Dataset<'a>>,\n    /// The widget base style\n    style: Style,\n    /// The legend's style.\n    legend_style: Style,\n    /// Constraints used to determine whether the legend should be shown or not\n    hidden_legend_constraints: (Constraint, Constraint),\n    /// The position determining whether the length is shown or hidden, regardless\n    /// of `hidden_legend_constraints`\n    legend_position: Option<LegendPosition>,\n    /// The marker type.\n    marker: Marker,\n    /// Whether to scale the values differently.\n    scaling: ChartScaling,\n}\n\nimpl<'a> TimeChart<'a> {\n    /// Creates a chart with the given [datasets](Dataset).\n    pub fn new(datasets: Vec<Dataset<'a>>) -> TimeChart<'a> {\n        TimeChart {\n            block: None,\n            x_axis: Axis::default(),\n            y_axis: Axis::default(),\n            style: Style::default(),\n            legend_style: Style::default(),\n            datasets,\n            hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),\n            legend_position: Some(LegendPosition::default()),\n            marker: Marker::Braille,\n            scaling: ChartScaling::default(),\n        }\n    }\n\n    /// Wraps the chart with the given [`Block`]\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn block(mut self, block: Block<'a>) -> TimeChart<'a> {\n        self.block = Some(block);\n        self\n    }\n\n    /// Sets the style of the entire chart\n    ///\n    /// `style` accepts any type that is convertible to [`Style`] (e.g.\n    /// [`Style`], [`Color`], or your own type that implements\n    /// [`Into<Style>`]).\n    ///\n    /// Styles of [`Axis`] and [`Dataset`] will have priority over this style.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn style<S: Into<Style>>(mut self, style: S) -> TimeChart<'a> {\n        self.style = style.into();\n        self\n    }\n\n    /// Sets the legend's style.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn legend_style(mut self, legend_style: Style) -> TimeChart<'a> {\n        self.legend_style = legend_style;\n        self\n    }\n\n    /// Sets the X [`Axis`]\n    ///\n    /// The default is an empty [`Axis`], i.e. only a line.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn x_axis(mut self, axis: Axis<'a>) -> TimeChart<'a> {\n        self.x_axis = axis;\n        self\n    }\n\n    /// Sets the Y [`Axis`]\n    ///\n    /// The default is an empty [`Axis`], i.e. only a line.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn y_axis(mut self, axis: Axis<'a>) -> TimeChart<'a> {\n        self.y_axis = axis;\n        self\n    }\n\n    /// Sets the marker type.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn marker(mut self, marker: Marker) -> TimeChart<'a> {\n        self.marker = marker;\n        self\n    }\n\n    /// Sets the constraints used to determine whether the legend should be\n    /// shown or not.\n    ///\n    /// The tuple's first constraint is used for the width and the second for\n    /// the height. If the legend takes more space than what is allowed by\n    /// any constraint, the legend is hidden. [`Constraint::Min`] is an\n    /// exception and will always show the legend.\n    ///\n    /// If this is not set, the default behavior is to hide the legend if it is\n    /// greater than 25% of the chart, either horizontally or vertically.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn hidden_legend_constraints(\n        mut self, constraints: (Constraint, Constraint),\n    ) -> TimeChart<'a> {\n        self.hidden_legend_constraints = constraints;\n        self\n    }\n\n    /// Sets the position of a legend or hide it.\n    ///\n    /// The default is [`LegendPosition::TopRight`].\n    ///\n    /// If [`None`] is given, hide the legend even if\n    /// [`hidden_legend_constraints`] determines it should be shown. In\n    /// contrast, if `Some(...)` is given, [`hidden_legend_constraints`] might\n    /// still decide whether to show the legend or not.\n    ///\n    /// See [`LegendPosition`] for all available positions.\n    ///\n    /// [`hidden_legend_constraints`]: Self::hidden_legend_constraints\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn legend_position(mut self, position: Option<LegendPosition>) -> TimeChart<'a> {\n        self.legend_position = position;\n        self\n    }\n\n    /// Set chart scaling.\n    #[must_use = \"method moves the value of self and returns the modified value\"]\n    pub fn scaling(mut self, scaling: ChartScaling) -> TimeChart<'a> {\n        self.scaling = scaling;\n        self\n    }\n\n    /// Compute the internal layout of the chart given the area. If the area is\n    /// too small some elements may be automatically hidden\n    fn layout(&self, area: Rect) -> ChartLayout {\n        let mut layout = ChartLayout::default();\n        if area.height == 0 || area.width == 0 {\n            return layout;\n        }\n        let mut x = area.left();\n        let mut y = area.bottom() - 1;\n\n        if self.x_axis.labels.is_some() && y > area.top() {\n            layout.label_x = Some(y);\n            y -= 1;\n        }\n\n        layout.label_y = self.y_axis.labels.as_ref().and(Some(x));\n        x += self.max_width_of_labels_left_of_y_axis(area, self.y_axis.labels.is_some());\n\n        if self.x_axis.labels.is_some() && y > area.top() {\n            layout.axis_x = Some(y);\n            y -= 1;\n        }\n\n        if self.y_axis.labels.is_some() && x + 1 < area.right() {\n            layout.axis_y = Some(x);\n            x += 1;\n        }\n\n        if x < area.right() && y > 1 {\n            layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);\n        }\n\n        if let Some(ref title) = self.x_axis.title {\n            let w = title.width() as u16;\n            if w < layout.graph_area.width && layout.graph_area.height > 2 {\n                layout.title_x = Some((x + layout.graph_area.width - w, y));\n            }\n        }\n\n        if let Some(ref title) = self.y_axis.title {\n            let w = title.width() as u16;\n            if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {\n                layout.title_y = Some((x, area.top()));\n            }\n        }\n\n        if let Some(legend_position) = self.legend_position {\n            let legends = self\n                .datasets\n                .iter()\n                .filter_map(|d| Some(d.name.as_ref()?.width() as u16));\n\n            if let Some(inner_width) = legends.clone().max() {\n                let legend_width = inner_width + 2;\n                let legend_height = legends.count() as u16 + 2;\n\n                let [max_legend_width] = Layout::horizontal([self.hidden_legend_constraints.0])\n                    .flex(Flex::Start)\n                    .areas(layout.graph_area);\n\n                let [max_legend_height] = Layout::vertical([self.hidden_legend_constraints.1])\n                    .flex(Flex::Start)\n                    .areas(layout.graph_area);\n\n                if inner_width > 0\n                    && legend_width <= max_legend_width.width\n                    && legend_height <= max_legend_height.height\n                {\n                    layout.legend_area = legend_position.layout(\n                        layout.graph_area,\n                        legend_width,\n                        legend_height,\n                        layout\n                            .title_x\n                            .and(self.x_axis.title.as_ref())\n                            .map(|t| t.width() as u16)\n                            .unwrap_or_default(),\n                        layout\n                            .title_y\n                            .and(self.y_axis.title.as_ref())\n                            .map(|t| t.width() as u16)\n                            .unwrap_or_default(),\n                    );\n                }\n            }\n        }\n        layout\n    }\n\n    fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {\n        let mut max_width = self\n            .y_axis\n            .labels\n            .as_ref()\n            .map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)\n            .unwrap_or_default();\n\n        if let Some(first_x_label) = self\n            .x_axis\n            .labels\n            .as_ref()\n            .and_then(|labels| labels.first())\n        {\n            let first_label_width = first_x_label.content.width() as u16;\n            let width_left_of_y_axis = match self.x_axis.labels_alignment {\n                Alignment::Left => {\n                    // The last character of the label should be below the Y-Axis when it exists,\n                    // not on its left\n                    let y_axis_offset = u16::from(has_y_axis);\n                    first_label_width.saturating_sub(y_axis_offset)\n                }\n                Alignment::Center => first_label_width / 2,\n                Alignment::Right => 0,\n            };\n            max_width = max(max_width, width_left_of_y_axis);\n        }\n        // labels of y axis and first label of x axis can take at most 1/3rd of the\n        // total width\n        max_width.min(area.width / 3)\n    }\n\n    fn render_x_labels(\n        &self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, graph_area: Rect,\n    ) {\n        let Some(y) = layout.label_x else { return };\n        let Some(labels) = self.x_axis.labels.as_ref() else {\n            return;\n        };\n        let labels_len = labels.len() as u16;\n        if labels_len < 2 {\n            return;\n        }\n\n        let first_label = labels.first().expect(\"must have at least 2 labels\");\n        let last_label = labels.last().expect(\"must have at least 2 labels\");\n\n        let width_between_ticks = graph_area.width / labels_len;\n\n        let label_area = self.first_x_label_area(\n            y,\n            first_label.width() as u16,\n            width_between_ticks,\n            chart_area,\n            graph_area,\n        );\n\n        let label_alignment = match self.x_axis.labels_alignment {\n            Alignment::Left => Alignment::Right,\n            Alignment::Center => Alignment::Center,\n            Alignment::Right => Alignment::Left,\n        };\n\n        Self::render_label(buf, first_label, label_area, label_alignment);\n\n        for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {\n            // We add 1 to x (and width-1 below) to leave at least one space before each\n            // intermediate labels\n            let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;\n            let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);\n\n            Self::render_label(buf, label, label_area, Alignment::Center);\n        }\n\n        let x = graph_area.right() - width_between_ticks;\n        let label_area = Rect::new(x, y, width_between_ticks, 1);\n        // The last label should be aligned Right to be at the edge of the graph area\n        Self::render_label(buf, last_label, label_area, Alignment::Right);\n    }\n\n    fn first_x_label_area(\n        &self, y: u16, label_width: u16, max_width_after_y_axis: u16, chart_area: Rect,\n        graph_area: Rect,\n    ) -> Rect {\n        let (min_x, max_x) = match self.x_axis.labels_alignment {\n            Alignment::Left => (chart_area.left(), graph_area.left()),\n            Alignment::Center => (\n                chart_area.left(),\n                graph_area.left() + max_width_after_y_axis.min(label_width),\n            ),\n            Alignment::Right => (\n                graph_area.left().saturating_sub(1),\n                graph_area.left() + max_width_after_y_axis,\n            ),\n        };\n\n        Rect::new(min_x, y, max_x - min_x, 1)\n    }\n\n    fn render_label(buf: &mut Buffer, label: &Span<'_>, label_area: Rect, alignment: Alignment) {\n        let label_width = label.width() as u16;\n        let bounded_label_width = label_area.width.min(label_width);\n\n        let x = match alignment {\n            Alignment::Left => label_area.left(),\n            Alignment::Center => label_area.left() + label_area.width / 2 - bounded_label_width / 2,\n            Alignment::Right => label_area.right() - bounded_label_width,\n        };\n\n        buf.set_span(x, label_area.top(), label, bounded_label_width);\n    }\n\n    fn render_y_labels(\n        &self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, graph_area: Rect,\n    ) {\n        // FIXME: Control how many y-axis labels are rendered based on height.\n\n        let Some(x) = layout.label_y else { return };\n        let Some(labels) = self.y_axis.labels.as_ref() else {\n            return;\n        };\n        let labels_len = labels.len() as u16;\n\n        for (i, label) in labels.iter().enumerate() {\n            let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);\n            if dy < graph_area.bottom() {\n                let label_area = Rect::new(\n                    x,\n                    graph_area.bottom().saturating_sub(1) - dy,\n                    (graph_area.left() - chart_area.left()).saturating_sub(1),\n                    1,\n                );\n                Self::render_label(buf, label, label_area, self.y_axis.labels_alignment);\n            }\n        }\n    }\n}\n\nimpl Widget for TimeChart<'_> {\n    fn render(self, area: Rect, buf: &mut Buffer) {\n        buf.set_style(area, self.style);\n\n        self.block.as_ref().render(area, buf);\n        let chart_area = self.block.inner_if_some(area);\n        if chart_area.is_empty() {\n            return;\n        }\n\n        // Sample the style of the entire widget. This sample will be used to reset the\n        // style of the cells that are part of the components put on top of the\n        // graph area (i.e legend and axis names).\n        let Some(original_style) = buf.cell((area.left(), area.top())).map(|cell| cell.style())\n        else {\n            return;\n        };\n\n        let layout = self.layout(chart_area);\n        let graph_area = layout.graph_area;\n        if graph_area.width < 1 || graph_area.height < 1 {\n            return;\n        }\n\n        self.render_x_labels(buf, &layout, chart_area, graph_area);\n        self.render_y_labels(buf, &layout, chart_area, graph_area);\n\n        if let Some(y) = layout.axis_x {\n            for x in graph_area.left()..graph_area.right() {\n                if let Some(cell) = buf.cell_mut((x, y)) {\n                    cell.set_symbol(symbols::line::HORIZONTAL)\n                        .set_style(self.x_axis.style);\n                }\n            }\n        }\n\n        if let Some(x) = layout.axis_y {\n            for y in graph_area.top()..graph_area.bottom() {\n                if let Some(cell) = buf.cell_mut((x, y)) {\n                    cell.set_symbol(symbols::line::VERTICAL)\n                        .set_style(self.y_axis.style);\n                }\n            }\n        }\n\n        if let Some(y) = layout.axis_x {\n            if let Some(x) = layout.axis_y {\n                if let Some(cell) = buf.cell_mut((x, y)) {\n                    cell.set_symbol(symbols::line::BOTTOM_LEFT)\n                        .set_style(self.x_axis.style);\n                }\n            }\n        }\n\n        let x_bounds = self.x_axis.bounds.get_bounds();\n        let y_bounds = self.y_axis.bounds.get_bounds();\n\n        Canvas::default()\n            .background_color(self.style.bg.unwrap_or(Color::Reset))\n            .x_bounds(x_bounds)\n            .y_bounds(y_bounds)\n            .marker(self.marker)\n            .paint(|ctx| {\n                self.draw_points(ctx);\n            })\n            .render(graph_area, buf);\n\n        if let Some((x, y)) = layout.title_x {\n            if let Some(title) = self.x_axis.title.as_ref() {\n                let width = graph_area\n                    .right()\n                    .saturating_sub(x)\n                    .min(title.width() as u16);\n                buf.set_style(\n                    Rect {\n                        x,\n                        y,\n                        width,\n                        height: 1,\n                    },\n                    original_style,\n                );\n                buf.set_line(x, y, title, width);\n            }\n        }\n\n        if let Some((x, y)) = layout.title_y {\n            if let Some(title) = self.y_axis.title.as_ref() {\n                let width = graph_area\n                    .right()\n                    .saturating_sub(x)\n                    .min(title.width() as u16);\n                buf.set_style(\n                    Rect {\n                        x,\n                        y,\n                        width,\n                        height: 1,\n                    },\n                    original_style,\n                );\n                buf.set_line(x, y, title, width);\n            }\n        }\n\n        if let Some(legend_area) = layout.legend_area {\n            buf.set_style(legend_area, original_style);\n            let block = Block::default()\n                .borders(Borders::ALL)\n                .border_style(self.legend_style);\n            for pos in block.inner(legend_area).positions() {\n                if let Some(cell) = buf.cell_mut(pos) {\n                    cell.set_symbol(\" \");\n                }\n            }\n            block.render(legend_area, buf);\n\n            for (i, (dataset_name, dataset_style)) in self\n                .datasets\n                .iter()\n                .filter_map(|ds| Some((ds.name.as_ref()?, ds.style())))\n                .enumerate()\n            {\n                let name = dataset_name.clone().patch_style(dataset_style);\n                name.render(\n                    Rect {\n                        x: legend_area.x + 1,\n                        y: legend_area.y + 1 + i as u16,\n                        width: legend_area.width - 2,\n                        height: 1,\n                    },\n                    buf,\n                );\n            }\n        }\n    }\n}\n\nimpl<'a> Styled for Axis<'a> {\n    type Item = Axis<'a>;\n\n    fn style(&self) -> Style {\n        self.style\n    }\n\n    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {\n        self.style(style)\n    }\n}\n\nimpl<'a> Styled for Dataset<'a> {\n    type Item = Dataset<'a>;\n\n    fn style(&self) -> Style {\n        self.style\n    }\n\n    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {\n        self.style(style)\n    }\n}\n\nimpl<'a> Styled for TimeChart<'a> {\n    type Item = TimeChart<'a>;\n\n    fn style(&self) -> Style {\n        self.style\n    }\n\n    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {\n        self.style(style)\n    }\n}\n\n/// Tests taken from ratatui.\n#[cfg(test)]\nmod tests {\n    macro_rules! assert_buffer_eq {\n        ($actual_expr:expr, $expected_expr:expr) => {\n            match (&$actual_expr, &$expected_expr) {\n                (actual, expected) => {\n                    if actual.area != expected.area {\n                        panic!(\n                            indoc::indoc!(\n                                \"\n                                buffer areas not equal\n                                expected:  {:?}\n                                actual:    {:?}\"\n                            ),\n                            expected, actual\n                        );\n                    }\n                    let diff = expected.diff(&actual);\n                    if !diff.is_empty() {\n                        let nice_diff = diff\n                            .iter()\n                            .enumerate()\n                            .map(|(i, (x, y, cell))| {\n                                let expected_cell = expected.cell((*x, *y)).unwrap();\n                                indoc::formatdoc! {\"\n                                    {i}: at ({x}, {y})\n                                      expected: {expected_cell:?}\n                                      actual:   {cell:?}\n                                \"}\n                            })\n                            .collect::<Vec<String>>()\n                            .join(\"\\n\");\n                        panic!(\n                            indoc::indoc!(\n                                \"\n                                buffer contents not equal\n                                expected: {:?}\n                                actual: {:?}\n                                diff:\n                                {}\"\n                            ),\n                            expected, actual, nice_diff\n                        );\n                    }\n                    // shouldn't get here, but this guards against future behavior\n                    // that changes equality but not area or content\n                    assert_eq!(actual, expected, \"buffers not equal\");\n                }\n            }\n        };\n    }\n\n    use std::time::Duration;\n\n    use tui::style::{Modifier, Stylize};\n\n    use super::*;\n\n    struct LegendTestCase {\n        chart_area: Rect,\n        hidden_legend_constraints: (Constraint, Constraint),\n        legend_area: Option<Rect>,\n    }\n\n    #[test]\n    fn it_should_hide_the_legend() {\n        let now = Instant::now();\n        let times = [\n            now,\n            now.checked_add(Duration::from_secs(1)).unwrap(),\n            now.checked_add(Duration::from_secs(2)).unwrap(),\n        ];\n        let mut values = Values::default();\n        values.push(5.0);\n        values.push(6.0);\n        values.push(7.0);\n\n        let cases = [\n            LegendTestCase {\n                chart_area: Rect::new(0, 0, 100, 100),\n                hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),\n                legend_area: Some(Rect::new(88, 0, 12, 12)),\n            },\n            LegendTestCase {\n                chart_area: Rect::new(0, 0, 100, 100),\n                hidden_legend_constraints: (Constraint::Ratio(1, 10), Constraint::Ratio(1, 4)),\n                legend_area: None,\n            },\n        ];\n        for case in &cases {\n            let datasets = (0..10)\n                .map(|i| {\n                    let name = format!(\"Dataset #{i}\");\n                    Dataset::default().name(name).data(&times, &values)\n                })\n                .collect::<Vec<_>>();\n            let chart = TimeChart::new(datasets)\n                .x_axis(Axis::default().title(\"X axis\"))\n                .y_axis(Axis::default().title(\"Y axis\"))\n                .hidden_legend_constraints(case.hidden_legend_constraints);\n            let layout = chart.layout(case.chart_area);\n            assert_eq!(layout.legend_area, case.legend_area);\n        }\n    }\n\n    #[test]\n    fn axis_can_be_stylized() {\n        assert_eq!(\n            Axis::default().black().on_white().bold().not_dim().style,\n            Style::default()\n                .fg(Color::Black)\n                .bg(Color::White)\n                .add_modifier(Modifier::BOLD)\n                .remove_modifier(Modifier::DIM)\n        )\n    }\n\n    #[test]\n    fn dataset_can_be_stylized() {\n        assert_eq!(\n            Dataset::default().black().on_white().bold().not_dim().style,\n            Style::default()\n                .fg(Color::Black)\n                .bg(Color::White)\n                .add_modifier(Modifier::BOLD)\n                .remove_modifier(Modifier::DIM)\n        )\n    }\n\n    #[test]\n    fn chart_can_be_stylized() {\n        assert_eq!(\n            TimeChart::new(vec![])\n                .black()\n                .on_white()\n                .bold()\n                .not_dim()\n                .style,\n            Style::default()\n                .fg(Color::Black)\n                .bg(Color::White)\n                .add_modifier(Modifier::BOLD)\n                .remove_modifier(Modifier::DIM)\n        )\n    }\n\n    #[test]\n    fn graph_type_to_string() {\n        assert_eq!(GraphType::Scatter.to_string(), \"Scatter\");\n        assert_eq!(GraphType::Line.to_string(), \"Line\");\n    }\n\n    #[test]\n    fn graph_type_from_str() {\n        assert_eq!(\"Scatter\".parse::<GraphType>(), Ok(GraphType::Scatter));\n        assert_eq!(\"Line\".parse::<GraphType>(), Ok(GraphType::Line));\n        assert!(\"\".parse::<GraphType>().is_err());\n    }\n\n    #[test]\n    fn it_does_not_panic_if_title_is_wider_than_buffer() {\n        let widget = TimeChart::default()\n            .y_axis(Axis::default().title(\"xxxxxxxxxxxxxxxx\"))\n            .x_axis(Axis::default().title(\"xxxxxxxxxxxxxxxx\"));\n        let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 4));\n        widget.render(buffer.area, &mut buffer);\n\n        assert_eq!(buffer, Buffer::with_lines(vec![\" \".repeat(8); 4]))\n    }\n\n    #[test]\n    fn datasets_without_name_do_not_contribute_to_legend_height() {\n        let data_named_1 = Dataset::default().name(\"data1\"); // must occupy a row in legend\n        let data_named_2 = Dataset::default().name(\"\"); // must occupy a row in legend, even if name is empty\n        let data_unnamed = Dataset::default(); // must not occupy a row in legend\n        let widget = TimeChart::new(vec![data_named_1, data_unnamed, data_named_2]);\n        let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));\n        let layout = widget.layout(buffer.area);\n\n        assert!(layout.legend_area.is_some());\n        assert_eq!(layout.legend_area.unwrap().height, 4); // 2 for borders, 2\n        // for rows\n    }\n\n    #[test]\n    fn no_legend_if_no_named_datasets() {\n        let dataset = Dataset::default();\n        let widget = TimeChart::new(vec![dataset; 3]);\n        let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));\n        let layout = widget.layout(buffer.area);\n\n        assert!(layout.legend_area.is_none());\n    }\n\n    #[test]\n    fn dataset_legend_style_is_patched() {\n        let long_dataset_name = Dataset::default().name(\"Very long name\");\n        let short_dataset =\n            Dataset::default().name(Line::from(\"Short name\").alignment(Alignment::Right));\n        let widget = TimeChart::new(vec![long_dataset_name, short_dataset])\n            .hidden_legend_constraints((100.into(), 100.into()));\n        let mut buffer = Buffer::empty(Rect::new(0, 0, 20, 5));\n\n        widget.render(buffer.area, &mut buffer);\n\n        let expected = Buffer::with_lines(vec![\n            \"    ┌──────────────┐\",\n            \"    │Very long name│\",\n            \"    │    Short name│\",\n            \"    └──────────────┘\",\n            \"                    \",\n        ]);\n        assert_buffer_eq!(buffer, expected);\n    }\n\n    #[test]\n    fn test_chart_have_a_topleft_legend() {\n        let chart = TimeChart::new(vec![Dataset::default().name(\"Ds1\")])\n            .legend_position(Some(LegendPosition::TopLeft));\n\n        let area = Rect::new(0, 0, 30, 20);\n        let mut buffer = Buffer::empty(area);\n\n        chart.render(buffer.area, &mut buffer);\n\n        let expected = Buffer::with_lines(vec![\n            \"┌───┐                         \",\n            \"│Ds1│                         \",\n            \"└───┘                         \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n        ]);\n\n        assert_eq!(buffer, expected);\n    }\n\n    #[test]\n    fn test_chart_have_a_long_y_axis_title_overlapping_legend() {\n        let chart = TimeChart::new(vec![Dataset::default().name(\"Ds1\")])\n            .y_axis(Axis::default().title(\"The title overlap a legend.\"));\n\n        let area = Rect::new(0, 0, 30, 20);\n        let mut buffer = Buffer::empty(area);\n\n        chart.render(buffer.area, &mut buffer);\n\n        let expected = Buffer::with_lines(vec![\n            \"The title overlap a legend.   \",\n            \"                         ┌───┐\",\n            \"                         │Ds1│\",\n            \"                         └───┘\",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n            \"                              \",\n        ]);\n\n        assert_eq!(buffer, expected);\n    }\n\n    #[test]\n    fn test_chart_have_overflowed_y_axis() {\n        let chart = TimeChart::new(vec![Dataset::default().name(\"Ds1\")])\n            .y_axis(Axis::default().title(\"The title overlap a legend.\"));\n\n        let area = Rect::new(0, 0, 10, 10);\n        let mut buffer = Buffer::empty(area);\n\n        chart.render(buffer.area, &mut buffer);\n\n        let expected = Buffer::with_lines(vec![\n            \"          \",\n            \"          \",\n            \"          \",\n            \"          \",\n            \"          \",\n            \"          \",\n            \"          \",\n            \"          \",\n            \"          \",\n            \"          \",\n        ]);\n\n        assert_eq!(buffer, expected);\n    }\n\n    #[test]\n    fn test_legend_area_can_fit_same_chart_area() {\n        let name = \"Data\";\n        let chart = TimeChart::new(vec![Dataset::default().name(name)])\n            .hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));\n\n        let area = Rect::new(0, 0, name.len() as u16 + 2, 3);\n        let mut buffer = Buffer::empty(area);\n\n        let expected = Buffer::with_lines(vec![\"┌────┐\", \"│Data│\", \"└────┘\"]);\n\n        [\n            LegendPosition::TopLeft,\n            LegendPosition::Top,\n            LegendPosition::TopRight,\n            LegendPosition::Left,\n            LegendPosition::Right,\n            LegendPosition::Bottom,\n            LegendPosition::BottomLeft,\n            LegendPosition::BottomRight,\n        ]\n        .iter()\n        .for_each(|&position| {\n            let chart = chart.clone().legend_position(Some(position));\n            buffer.reset();\n            chart.render(buffer.area, &mut buffer);\n            assert_eq!(buffer, expected);\n        });\n    }\n\n    #[test]\n    fn test_legend_of_chart_have_odd_margin_size() {\n        let name = \"Data\";\n        let base_chart = TimeChart::new(vec![Dataset::default().name(name)])\n            .hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));\n\n        let area = Rect::new(0, 0, name.len() as u16 + 2 + 3, 3 + 3);\n        let mut buffer = Buffer::empty(area);\n\n        let chart = base_chart\n            .clone()\n            .legend_position(Some(LegendPosition::TopLeft));\n        buffer.reset();\n        chart.render(buffer.area, &mut buffer);\n        assert_eq!(\n            buffer,\n            Buffer::with_lines(vec![\n                \"┌────┐   \",\n                \"│Data│   \",\n                \"└────┘   \",\n                \"         \",\n                \"         \",\n                \"         \",\n            ])\n        );\n        buffer.reset();\n\n        let chart = base_chart\n            .clone()\n            .legend_position(Some(LegendPosition::Top));\n        buffer.reset();\n        chart.render(buffer.area, &mut buffer);\n        assert_eq!(\n            buffer,\n            Buffer::with_lines(vec![\n                \" ┌────┐  \",\n                \" │Data│  \",\n                \" └────┘  \",\n                \"         \",\n                \"         \",\n                \"         \",\n            ])\n        );\n\n        let chart = base_chart\n            .clone()\n            .legend_position(Some(LegendPosition::TopRight));\n        buffer.reset();\n        chart.render(buffer.area, &mut buffer);\n        assert_eq!(\n            buffer,\n            Buffer::with_lines(vec![\n                \"   ┌────┐\",\n                \"   │Data│\",\n                \"   └────┘\",\n                \"         \",\n                \"         \",\n                \"         \",\n            ])\n        );\n\n        let chart = base_chart\n            .clone()\n            .legend_position(Some(LegendPosition::Left));\n        buffer.reset();\n        chart.render(buffer.area, &mut buffer);\n        assert_eq!(\n            buffer,\n            Buffer::with_lines(vec![\n                \"         \",\n                \"┌────┐   \",\n                \"│Data│   \",\n                \"└────┘   \",\n                \"         \",\n                \"         \",\n            ])\n        );\n        buffer.reset();\n\n        let chart = base_chart\n            .clone()\n            .legend_position(Some(LegendPosition::Right));\n        buffer.reset();\n        chart.render(buffer.area, &mut buffer);\n        assert_eq!(\n            buffer,\n            Buffer::with_lines(vec![\n                \"         \",\n                \"   ┌────┐\",\n                \"   │Data│\",\n                \"   └────┘\",\n                \"         \",\n                \"         \",\n            ])\n        );\n\n        let chart = base_chart\n            .clone()\n            .legend_position(Some(LegendPosition::BottomLeft));\n        buffer.reset();\n        chart.render(buffer.area, &mut buffer);\n        assert_eq!(\n            buffer,\n            Buffer::with_lines(vec![\n                \"         \",\n                \"         \",\n                \"         \",\n                \"┌────┐   \",\n                \"│Data│   \",\n                \"└────┘   \",\n            ])\n        );\n\n        let chart = base_chart\n            .clone()\n            .legend_position(Some(LegendPosition::Bottom));\n        buffer.reset();\n        chart.render(buffer.area, &mut buffer);\n        assert_eq!(\n            buffer,\n            Buffer::with_lines(vec![\n                \"         \",\n                \"         \",\n                \"         \",\n                \" ┌────┐  \",\n                \" │Data│  \",\n                \" └────┘  \",\n            ])\n        );\n\n        let chart = base_chart\n            .clone()\n            .legend_position(Some(LegendPosition::BottomRight));\n        buffer.reset();\n        chart.render(buffer.area, &mut buffer);\n        assert_eq!(\n            buffer,\n            Buffer::with_lines(vec![\n                \"         \",\n                \"         \",\n                \"         \",\n                \"   ┌────┐\",\n                \"   │Data│\",\n                \"   └────┘\",\n            ])\n        );\n\n        let chart = base_chart.clone().legend_position(None);\n        buffer.reset();\n        chart.render(buffer.area, &mut buffer);\n        assert_eq!(\n            buffer,\n            Buffer::with_lines(vec![\n                \"         \",\n                \"         \",\n                \"         \",\n                \"         \",\n                \"         \",\n                \"         \",\n            ])\n        );\n    }\n}\n"
  },
  {
    "path": "src/canvas/components/time_graph.rs",
    "content": "//! A graph displaying data in the y-axis over time in the x-axis.\n//!\n//! A \"base\" version is available, based on a vendored version of\n//! ratatui's [charts](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html),\n//! as are variants for common use cases.\n\nmod base;\nmod variants;\nmod vendored;\n\npub(crate) use base::*;\npub(crate) use variants::percent::PercentTimeGraph;\npub(crate) use vendored::*;\n"
  },
  {
    "path": "src/canvas/components/widget_carousel.rs",
    "content": "use tui::{\n    Frame,\n    layout::{Alignment, Constraint, Direction, Layout, Rect},\n    text::{Line, Span},\n    widgets::{Block, Paragraph},\n};\n\nuse crate::{\n    app::{App, layout_manager::BottomWidgetType},\n    canvas::Painter,\n};\n\nimpl Painter {\n    pub fn draw_basic_table_arrows(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        if let Some(current_table) = app_state.widget_map.get(&widget_id) {\n            let current_table = if let BottomWidgetType::ProcSort = current_table.widget_type {\n                current_table\n                    .right_neighbour\n                    .and_then(|id| app_state.widget_map.get(&id))\n                    .expect(\"id must exist in widget mapping\")\n            } else {\n                current_table\n            };\n\n            let (left_table, right_table) = (\n                {\n                    current_table\n                        .left_neighbour\n                        .map(|left_widget_id| {\n                            app_state\n                                .widget_map\n                                .get(&left_widget_id)\n                                .map(|left_widget| {\n                                    if left_widget.widget_type == BottomWidgetType::ProcSort {\n                                        left_widget\n                                            .left_neighbour\n                                            .map(|left_left_widget_id| {\n                                                app_state.widget_map.get(&left_left_widget_id).map(\n                                                    |left_left_widget| {\n                                                        &left_left_widget.widget_type\n                                                    },\n                                                )\n                                            })\n                                            .unwrap_or(Some(&BottomWidgetType::Temp))\n                                            .unwrap_or(&BottomWidgetType::Temp)\n                                    } else {\n                                        &left_widget.widget_type\n                                    }\n                                })\n                                .unwrap_or(&BottomWidgetType::Temp)\n                        })\n                        .unwrap_or(&BottomWidgetType::Temp)\n                },\n                {\n                    current_table\n                        .right_neighbour\n                        .map(|right_widget_id| {\n                            app_state\n                                .widget_map\n                                .get(&right_widget_id)\n                                .map(|right_widget| {\n                                    if right_widget.widget_type == BottomWidgetType::ProcSort {\n                                        right_widget\n                                            .right_neighbour\n                                            .map(|right_right_widget_id| {\n                                                app_state\n                                                    .widget_map\n                                                    .get(&right_right_widget_id)\n                                                    .map(|right_right_widget| {\n                                                        &right_right_widget.widget_type\n                                                    })\n                                            })\n                                            .unwrap_or(Some(&BottomWidgetType::Disk))\n                                            .unwrap_or(&BottomWidgetType::Disk)\n                                    } else {\n                                        &right_widget.widget_type\n                                    }\n                                })\n                                .unwrap_or(&BottomWidgetType::Disk)\n                        })\n                        .unwrap_or(&BottomWidgetType::Disk)\n                },\n            );\n\n            // TODO: I can do this text effect as just a border now!\n            let left_name = left_table.get_pretty_name();\n            let right_name = right_table.get_pretty_name();\n            let num_spaces =\n                usize::from(draw_loc.width).saturating_sub(6 + left_name.len() + right_name.len());\n            let carousel_text_style = if widget_id == app_state.current_widget.widget_id {\n                self.styles.highlighted_border_style\n            } else {\n                self.styles.text_style\n            };\n\n            let left_arrow_text = vec![\n                Line::default(),\n                Line::from(Span::styled(format!(\"◄ {left_name}\"), carousel_text_style)),\n            ];\n\n            let right_arrow_text = vec![\n                Line::default(),\n                Line::from(Span::styled(format!(\"{right_name} ►\"), carousel_text_style)),\n            ];\n\n            let margined_draw_loc = Layout::default()\n                .direction(Direction::Horizontal)\n                .constraints([\n                    Constraint::Length(2 + left_name.len() as u16),\n                    Constraint::Length(num_spaces as u16),\n                    Constraint::Length(2 + right_name.len() as u16),\n                ])\n                .horizontal_margin(1)\n                .split(draw_loc);\n\n            f.render_widget(\n                Paragraph::new(left_arrow_text).block(Block::default()),\n                margined_draw_loc[0],\n            );\n            f.render_widget(\n                Paragraph::new(right_arrow_text)\n                    .block(Block::default())\n                    .alignment(Alignment::Right),\n                margined_draw_loc[2],\n            );\n\n            if app_state.should_get_widget_bounds() {\n                // Some explanations for future readers:\n                // - The \"height\" as of writing of this entire widget is 2.  If it's 1, it\n                //   occasionally doesn't draw.\n                // - As such, the buttons are only on the lower part of this 2-high widget.\n                // - So, we want to only check at one location, the `draw_loc.y + 1`, and that's\n                //   it.\n                // - But why is it \"+2\" then?  Well, it's because I have a REALLY ugly hack\n                //   for mouse button checking, since most button checks are of the form `(draw_loc.y + draw_loc.height)`,\n                //   and the same for the x and width.  Unfortunately, if you check using >= and <=, the outer bound is\n                //   actually too large - so, we assume all of them are one too big and check via < (see\n                //   https://github.com/ClementTsang/bottom/pull/459 for details).\n                // - So in other words, to make it simple, we keep this to a standard and\n                //   overshoot by one here.\n                if let Some(basic_table) = &mut app_state.states.basic_table_widget_state {\n                    basic_table.left_tlc =\n                        Some((margined_draw_loc[0].x, margined_draw_loc[0].y + 1));\n                    basic_table.left_brc = Some((\n                        margined_draw_loc[0].x + margined_draw_loc[0].width,\n                        margined_draw_loc[0].y + 2,\n                    ));\n                    basic_table.right_tlc =\n                        Some((margined_draw_loc[2].x, margined_draw_loc[2].y + 1));\n                    basic_table.right_brc = Some((\n                        margined_draw_loc[2].x + margined_draw_loc[2].width,\n                        margined_draw_loc[2].y + 2,\n                    ));\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/dialogs/help_dialog.rs",
    "content": "use std::cmp::{max, min};\n\nuse tui::{\n    Frame,\n    layout::{Alignment, Rect},\n    text::{Line, Span},\n    widgets::{Paragraph, Wrap},\n};\nuse unicode_width::UnicodeWidthStr;\n\nuse crate::{\n    app::App,\n    canvas::{Painter, drawing_utils::dialog_block},\n    constants::{self, HELP_TEXT},\n};\n\n// TODO: [REFACTOR] Make generic dialog boxes to build off of instead?\nimpl Painter {\n    fn help_text_lines(&self) -> Vec<Line<'_>> {\n        let mut styled_help_spans = Vec::new();\n\n        // Init help text:\n        HELP_TEXT.iter().enumerate().for_each(|(itx, section)| {\n            let mut section = section.iter();\n\n            if itx > 0 {\n                if let Some(header) = section.next() {\n                    styled_help_spans.push(Span::default());\n                    styled_help_spans.push(Span::styled(*header, self.styles.table_header_style));\n                }\n            }\n\n            section.for_each(|&text| {\n                styled_help_spans.push(Span::styled(text, self.styles.text_style))\n            });\n        });\n\n        styled_help_spans.into_iter().map(Line::from).collect()\n    }\n\n    pub fn draw_help_dialog(&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect) {\n        let styled_help_text = self.help_text_lines();\n\n        let block = dialog_block(self.styles.border_type)\n            .border_style(self.styles.border_style)\n            .title_top(Line::styled(\" Help \", self.styles.widget_title_style))\n            .title_top(\n                Line::styled(\" Esc to close \", self.styles.widget_title_style).right_aligned(),\n            );\n\n        if app_state.should_get_widget_bounds() {\n            // We must also recalculate how many lines are wrapping to properly get\n            // scrolling to work on small terminal sizes... oh joy.\n\n            app_state.help_dialog_state.height = block.inner(draw_loc).height;\n\n            let mut overflow_buffer = 0;\n            let paragraph_width = max(draw_loc.width.saturating_sub(2), 1);\n            let mut prev_section_len = 0;\n\n            constants::HELP_TEXT\n                .iter()\n                .enumerate()\n                .for_each(|(itx, section)| {\n                    let mut buffer = 0;\n\n                    if itx == 0 {\n                        section.iter().for_each(|text_line| {\n                            buffer += UnicodeWidthStr::width(*text_line).saturating_sub(1) as u16\n                                / paragraph_width;\n                        });\n\n                        app_state.help_dialog_state.index_shortcuts[itx] = 0;\n                    } else {\n                        section.iter().for_each(|text_line| {\n                            buffer += UnicodeWidthStr::width(*text_line).saturating_sub(1) as u16\n                                / paragraph_width;\n                        });\n\n                        app_state.help_dialog_state.index_shortcuts[itx] =\n                            app_state.help_dialog_state.index_shortcuts[itx - 1]\n                                + 1\n                                + prev_section_len;\n                    }\n                    prev_section_len = section.len() as u16 + buffer;\n                    overflow_buffer += buffer;\n                });\n\n            let max_scroll_index = &mut app_state.help_dialog_state.scroll_state.max_scroll_index;\n            *max_scroll_index = (styled_help_text.len() as u16 + 3 + overflow_buffer)\n                .saturating_sub(draw_loc.height + 1);\n\n            // Fix the scroll index if it is over-scrolled\n            let index = &mut app_state\n                .help_dialog_state\n                .scroll_state\n                .current_scroll_index;\n\n            *index = min(*index, *max_scroll_index);\n        }\n\n        f.render_widget(\n            Paragraph::new(styled_help_text.clone())\n                .block(block)\n                .style(self.styles.text_style)\n                .alignment(Alignment::Left)\n                .wrap(Wrap { trim: true })\n                .scroll((\n                    app_state\n                        .help_dialog_state\n                        .scroll_state\n                        .current_scroll_index,\n                    0,\n                )),\n            draw_loc,\n        );\n    }\n}\n"
  },
  {
    "path": "src/canvas/dialogs/mod.rs",
    "content": "pub mod help_dialog;\npub mod process_kill_dialog;\n"
  },
  {
    "path": "src/canvas/dialogs/process_kill_dialog.rs",
    "content": "//! A dialog box to handle killing processes.\n\nuse std::time::Instant;\n\nuse cfg_if::cfg_if;\n#[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\nuse tui::widgets::ListState;\nuse tui::{\n    Frame,\n    layout::{Alignment, Constraint, Flex, Layout, Position, Rect},\n    text::{Line, Span, Text},\n    widgets::{Paragraph, Wrap},\n};\n\nuse crate::{\n    canvas::drawing_utils::dialog_block, collection::processes::Pid, options::config::style::Styles,\n};\n\n// Configure signal text based on the target OS.\ncfg_if! {\n    if #[cfg(target_os = \"linux\")] {\n        const DEFAULT_KILL_SIGNAL: usize = 15;\n        const SIGNAL_TEXT: [&str; 63] = [\n            \"0: Cancel\",\n            \"1: HUP\",\n            \"2: INT\",\n            \"3: QUIT\",\n            \"4: ILL\",\n            \"5: TRAP\",\n            \"6: ABRT\",\n            \"7: BUS\",\n            \"8: FPE\",\n            \"9: KILL\",\n            \"10: USR1\",\n            \"11: SEGV\",\n            \"12: USR2\",\n            \"13: PIPE\",\n            \"14: ALRM\",\n            \"15: TERM\",\n            \"16: STKFLT\",\n            \"17: CHLD\",\n            \"18: CONT\",\n            \"19: STOP\",\n            \"20: TSTP\",\n            \"21: TTIN\",\n            \"22: TTOU\",\n            \"23: URG\",\n            \"24: XCPU\",\n            \"25: XFSZ\",\n            \"26: VTALRM\",\n            \"27: PROF\",\n            \"28: WINCH\",\n            \"29: IO\",\n            \"30: PWR\",\n            \"31: SYS\",\n            \"34: RTMIN\",\n            \"35: RTMIN+1\",\n            \"36: RTMIN+2\",\n            \"37: RTMIN+3\",\n            \"38: RTMIN+4\",\n            \"39: RTMIN+5\",\n            \"40: RTMIN+6\",\n            \"41: RTMIN+7\",\n            \"42: RTMIN+8\",\n            \"43: RTMIN+9\",\n            \"44: RTMIN+10\",\n            \"45: RTMIN+11\",\n            \"46: RTMIN+12\",\n            \"47: RTMIN+13\",\n            \"48: RTMIN+14\",\n            \"49: RTMIN+15\",\n            \"50: RTMAX-14\",\n            \"51: RTMAX-13\",\n            \"52: RTMAX-12\",\n            \"53: RTMAX-11\",\n            \"54: RTMAX-10\",\n            \"55: RTMAX-9\",\n            \"56: RTMAX-8\",\n            \"57: RTMAX-7\",\n            \"58: RTMAX-6\",\n            \"59: RTMAX-5\",\n            \"60: RTMAX-4\",\n            \"61: RTMAX-3\",\n            \"62: RTMAX-2\",\n            \"63: RTMAX-1\",\n            \"64: RTMAX\",\n        ];\n    } else if #[cfg(target_os = \"macos\")] {\n        const DEFAULT_KILL_SIGNAL: usize = 15;\n        const SIGNAL_TEXT: [&str; 32] = [\n            \"0: Cancel\",\n            \"1: HUP\",\n            \"2: INT\",\n            \"3: QUIT\",\n            \"4: ILL\",\n            \"5: TRAP\",\n            \"6: ABRT\",\n            \"7: EMT\",\n            \"8: FPE\",\n            \"9: KILL\",\n            \"10: BUS\",\n            \"11: SEGV\",\n            \"12: SYS\",\n            \"13: PIPE\",\n            \"14: ALRM\",\n            \"15: TERM\",\n            \"16: URG\",\n            \"17: STOP\",\n            \"18: TSTP\",\n            \"19: CONT\",\n            \"20: CHLD\",\n            \"21: TTIN\",\n            \"22: TTOU\",\n            \"23: IO\",\n            \"24: XCPU\",\n            \"25: XFSZ\",\n            \"26: VTALRM\",\n            \"27: PROF\",\n            \"28: WINCH\",\n            \"29: INFO\",\n            \"30: USR1\",\n            \"31: USR2\",\n        ];\n    } else if #[cfg(target_os = \"freebsd\")] {\n        const DEFAULT_KILL_SIGNAL: usize = 15;\n        const SIGNAL_TEXT: [&str; 34] = [\n            \"0: Cancel\",\n            \"1: HUP\",\n            \"2: INT\",\n            \"3: QUIT\",\n            \"4: ILL\",\n            \"5: TRAP\",\n            \"6: ABRT\",\n            \"7: EMT\",\n            \"8: FPE\",\n            \"9: KILL\",\n            \"10: BUS\",\n            \"11: SEGV\",\n            \"12: SYS\",\n            \"13: PIPE\",\n            \"14: ALRM\",\n            \"15: TERM\",\n            \"16: URG\",\n            \"17: STOP\",\n            \"18: TSTP\",\n            \"19: CONT\",\n            \"20: CHLD\",\n            \"21: TTIN\",\n            \"22: TTOU\",\n            \"23: IO\",\n            \"24: XCPU\",\n            \"25: XFSZ\",\n            \"26: VTALRM\",\n            \"27: PROF\",\n            \"28: WINCH\",\n            \"29: INFO\",\n            \"30: USR1\",\n            \"31: USR2\",\n            \"32: THR\",\n            \"33: LIBRT\",\n        ];\n    }\n}\n\n/// Button state type for a [`ProcessKillDialog`].\n///\n/// Simple only has two buttons (yes/no), while signals (AKA advanced) are\n/// a list of signals to send.\n///\n/// Note that signals are not available for Windows.\n#[derive(Debug)]\npub(crate) enum ButtonState {\n    #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n    Signals {\n        state: ListState,\n        last_button_draw_area: Rect,\n    },\n    Simple {\n        yes: bool,\n        last_yes_button_area: Rect,\n        last_no_button_area: Rect,\n    },\n}\n\n#[derive(Debug)]\nstruct ProcessKillSelectingInner {\n    process_name: String,\n    pids: Vec<Pid>,\n    button_state: ButtonState,\n}\n\n/// The current state of the process kill dialog.\n#[derive(Default, Debug)]\nenum ProcessKillDialogState {\n    #[default]\n    NotEnabled,\n    Selecting(ProcessKillSelectingInner),\n    Error {\n        process_name: String,\n        pid: Option<Pid>,\n        err: String,\n    },\n}\n\n/// Process kill dialog.\n#[derive(Default, Debug)]\npub(crate) struct ProcessKillDialog {\n    state: ProcessKillDialogState,\n    last_char: Option<(char, Instant)>,\n}\n\nimpl ProcessKillDialog {\n    pub fn reset(&mut self) {\n        *self = Self::default();\n    }\n\n    #[inline]\n    pub fn is_open(&self) -> bool {\n        !(matches!(self.state, ProcessKillDialogState::NotEnabled))\n    }\n\n    pub fn on_esc(&mut self) {\n        self.reset();\n    }\n\n    pub fn on_enter(&mut self) {\n        // We do this to get around borrow issues.\n        let mut current = ProcessKillDialogState::NotEnabled;\n        std::mem::swap(&mut self.state, &mut current);\n\n        if let ProcessKillDialogState::Selecting(state) = current {\n            let process_name = state.process_name;\n            let button_state = state.button_state;\n            let pids = state.pids;\n\n            match button_state {\n                #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n                ButtonState::Signals { state, .. } => {\n                    use crate::utils::process_killer;\n\n                    if let Some(selected) = state.selected() {\n                        if selected != 0 {\n                            // On Linux, we need to skip 32 and 33.\n                            let signal = if cfg!(target_os = \"linux\")\n                                && (selected == 32 || selected == 33)\n                            {\n                                selected + 2\n                            } else {\n                                selected\n                            };\n\n                            for pid in pids {\n                                if let Err(err) =\n                                    process_killer::kill_process_given_pid(pid, signal)\n                                {\n                                    self.state = ProcessKillDialogState::Error {\n                                        process_name,\n                                        pid: Some(pid),\n                                        err: err.to_string(),\n                                    };\n                                    return;\n                                }\n                            }\n                        }\n                    }\n                }\n                ButtonState::Simple { yes, .. } => {\n                    if yes {\n                        cfg_if! {\n                            if #[cfg(target_os = \"windows\")] {\n                                use crate::utils::process_killer;\n\n                                for pid in pids {\n                                    if let Err(err) = process_killer::kill_process_given_pid(pid) {\n                                        self.state = ProcessKillDialogState::Error { process_name, pid: Some(pid), err: err.to_string() };\n                                        break;\n                                    }\n                                }\n                            } else if #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))] {\n                                use crate::utils::process_killer;\n\n                                for pid in pids {\n                                    // Send a SIGTERM by default.\n                                    if let Err(err) = process_killer::kill_process_given_pid(pid, DEFAULT_KILL_SIGNAL) {\n                                        self.state = ProcessKillDialogState::Error { process_name, pid: Some(pid), err: err.to_string() };\n                                        break;\n                                    }\n                                }\n                            } else {\n                                self.state = ProcessKillDialogState::Error { process_name, pid: None, err: \"Killing processes is not supported on this platform.\".into() };\n\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        // Fall through behaviour is just to close the dialog.\n        self.last_char = None;\n    }\n\n    pub fn on_char(&mut self, c: char) {\n        #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n        const MAX_KEY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);\n\n        match c {\n            'h' => self.on_left_key(),\n            'j' => self.on_down_key(),\n            'k' => self.on_up_key(),\n            'l' => self.on_right_key(),\n            '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {\n                #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n                if let Some(value) = c.to_digit(10) {\n                    if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n                        button_state: ButtonState::Signals { state, .. },\n                        ..\n                    }) = &mut self.state\n                    {\n                        if let Some((prev, last_press)) = self.last_char {\n                            if prev.is_ascii_digit() && last_press.elapsed() <= MAX_KEY_TIMEOUT {\n                                let current = state.selected().unwrap_or(0);\n                                let new = {\n                                    let new = current * 10 + value as usize;\n\n                                    // Note that 32 and 33 are skipped on linux.\n                                    if cfg!(target_os = \"linux\") {\n                                        if new == 32 || new == 33 {\n                                            value as usize\n                                        } else if new >= 34 {\n                                            new - 2\n                                        } else {\n                                            new\n                                        }\n                                    } else {\n                                        new\n                                    }\n                                };\n\n                                if new >= SIGNAL_TEXT.len() {\n                                    // If the new value is too large, then just assume we instead want the value itself.\n                                    state.select(Some(value as usize));\n                                    self.last_char = Some((c, Instant::now()));\n                                } else {\n                                    state.select(Some(new));\n                                    self.last_char = None;\n                                }\n                            } else {\n                                state.select(Some(value as usize));\n                                self.last_char = Some((c, Instant::now()));\n                            }\n                        } else {\n                            state.select(Some(value as usize));\n                            self.last_char = Some((c, Instant::now()));\n                        }\n\n                        return; // Needed to avoid accidentally clearing last_char.\n                    }\n                }\n            }\n            'g' => {\n                #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n                {\n                    if let Some(('g', last_press)) = self.last_char {\n                        if last_press.elapsed() <= MAX_KEY_TIMEOUT {\n                            self.go_to_first();\n                            self.last_char = None;\n                        } else {\n                            self.last_char = Some(('g', Instant::now()));\n                        }\n                    } else {\n                        self.last_char = Some(('g', Instant::now()));\n                    }\n                    return;\n                }\n            }\n            'G' => {\n                #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n                self.go_to_last();\n            }\n            _ => {}\n        }\n\n        self.last_char = None;\n    }\n\n    /// Handle a click at the given coordinates. Returns true if the click was\n    /// handled, false otherwise.\n    pub fn on_click(&mut self, x: u16, y: u16) -> bool {\n        if let ProcessKillDialogState::Selecting(state) = &mut self.state {\n            match &mut state.button_state {\n                #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n                ButtonState::Signals {\n                    state,\n                    last_button_draw_area,\n                } => {\n                    if last_button_draw_area.contains(Position { x, y }) {\n                        let relative_y =\n                            y.saturating_sub(last_button_draw_area.y) as usize + state.offset();\n                        if relative_y < SIGNAL_TEXT.len() {\n                            state.select(Some(relative_y));\n                        }\n                    }\n                }\n                ButtonState::Simple {\n                    yes,\n                    last_yes_button_area,\n                    last_no_button_area,\n                } => {\n                    if last_yes_button_area.contains(Position { x, y }) {\n                        *yes = true;\n                    } else if last_no_button_area.contains(Position { x, y }) {\n                        *yes = false;\n                    }\n                }\n            }\n        }\n\n        false\n    }\n\n    /// Scroll up in the signal list.\n    pub fn on_scroll_up(&mut self) {\n        self.on_up_key();\n    }\n\n    /// Scroll down in the signal list.\n    pub fn on_scroll_down(&mut self) {\n        self.on_down_key();\n    }\n\n    /// Handle a left key press.\n    pub fn on_left_key(&mut self) {\n        self.last_char = None;\n\n        if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n            button_state: ButtonState::Simple { yes, .. },\n            ..\n        }) = &mut self.state\n        {\n            *yes = true;\n        }\n    }\n\n    /// Handle a right key press.\n    pub fn on_right_key(&mut self) {\n        self.last_char = None;\n\n        if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n            button_state: ButtonState::Simple { yes, .. },\n            ..\n        }) = &mut self.state\n        {\n            *yes = false;\n        }\n    }\n\n    #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n    fn scroll_up_by(state: &mut ListState, amount: usize) {\n        if let Some(selected) = state.selected() {\n            if let Some(new_position) = selected.checked_sub(amount) {\n                state.select(Some(new_position));\n            } else {\n                state.select(Some(0));\n            }\n        }\n    }\n\n    #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n    fn scroll_down_by(state: &mut ListState, amount: usize) {\n        if let Some(selected) = state.selected() {\n            let new_position = selected + amount;\n            if new_position < SIGNAL_TEXT.len() {\n                state.select(Some(new_position));\n            } else {\n                state.select(Some(SIGNAL_TEXT.len() - 1));\n            }\n        }\n    }\n\n    /// Handle an up key press.\n    pub fn on_up_key(&mut self) {\n        self.last_char = None;\n\n        #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n        if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n            button_state: ButtonState::Signals { state, .. },\n            ..\n        }) = &mut self.state\n        {\n            Self::scroll_up_by(state, 1);\n        }\n    }\n\n    /// Handle a down key press.\n    pub fn on_down_key(&mut self) {\n        self.last_char = None;\n\n        #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n        if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n            button_state: ButtonState::Signals { state, .. },\n            ..\n        }) = &mut self.state\n        {\n            Self::scroll_down_by(state, 1);\n        }\n    }\n\n    // Handle page up.\n    pub fn on_page_up(&mut self) {\n        self.last_char = None;\n\n        #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n        if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n            button_state:\n                ButtonState::Signals {\n                    state,\n                    last_button_draw_area,\n                    ..\n                },\n            ..\n        }) = &mut self.state\n        {\n            Self::scroll_up_by(state, last_button_draw_area.height as usize);\n        }\n    }\n\n    /// Handle page down.\n    pub fn on_page_down(&mut self) {\n        self.last_char = None;\n\n        #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n        if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n            button_state:\n                ButtonState::Signals {\n                    state,\n                    last_button_draw_area,\n                    ..\n                },\n            ..\n        }) = &mut self.state\n        {\n            Self::scroll_down_by(state, last_button_draw_area.height as usize);\n        }\n    }\n\n    pub fn go_to_first(&mut self) {\n        self.last_char = None;\n\n        #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n        if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n            button_state: ButtonState::Signals { state, .. },\n            ..\n        }) = &mut self.state\n        {\n            state.select(Some(0));\n        }\n    }\n\n    pub fn go_to_last(&mut self) {\n        self.last_char = None;\n\n        #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n        if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n            button_state: ButtonState::Signals { state, .. },\n            ..\n        }) = &mut self.state\n        {\n            state.select(Some(SIGNAL_TEXT.len() - 1));\n        }\n    }\n\n    /// Enable the process kill process.\n    pub fn start_process_kill(\n        &mut self, process_name: String, pids: Vec<Pid>, use_simple_selection: bool,\n    ) {\n        let button_state = if use_simple_selection {\n            ButtonState::Simple {\n                yes: false,\n                last_yes_button_area: Rect::default(),\n                last_no_button_area: Rect::default(),\n            }\n        } else {\n            cfg_if! {\n                if #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))] {\n                    ButtonState::Signals { state: ListState::default().with_selected(Some(DEFAULT_KILL_SIGNAL)), last_button_draw_area: Rect::default() }\n                } else {\n                    ButtonState::Simple { yes: false, last_yes_button_area: Rect::default(), last_no_button_area: Rect::default()}\n                }\n            }\n        };\n\n        if pids.is_empty() {\n            self.state = ProcessKillDialogState::Error {\n                process_name,\n                pid: None,\n                err: \"No PIDs found for the given process name.\".into(),\n            };\n            return;\n        }\n\n        self.state = ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n            process_name,\n            pids,\n            button_state,\n        });\n    }\n\n    pub fn handle_redraw(&mut self) {\n        // FIXME: Not sure if we need this. We can probably handle this better in the draw function later.\n\n        #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n        {\n            if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner {\n                button_state: ButtonState::Signals { state, .. },\n                ..\n            }) = &mut self.state\n            {\n                // Fix the button offset state when we do things like resize.\n                *state.offset_mut() = 0;\n            }\n        }\n    }\n\n    #[inline]\n    fn draw_selecting(\n        f: &mut Frame<'_>, draw_area: Rect, styles: &Styles, state: &mut ProcessKillSelectingInner,\n    ) {\n        let ProcessKillSelectingInner {\n            process_name,\n            pids,\n            button_state,\n            ..\n        } = state;\n\n        // FIXME: Add some colour to this!\n        let text = {\n            const MAX_PROCESS_NAME_WIDTH: usize = 20;\n\n            if let Some(first_pid) = pids.first() {\n                let truncated_process_name =\n                    unicode_ellipsis::truncate_str(process_name, MAX_PROCESS_NAME_WIDTH);\n\n                let text = if pids.len() > 1 {\n                    Line::from(format!(\n                        \"Kill {} processes with the name '{}'? Press ENTER to confirm.\",\n                        pids.len(),\n                        truncated_process_name\n                    ))\n                } else {\n                    Line::from(format!(\n                        \"Kill process '{truncated_process_name}' with PID {first_pid}? Press ENTER to confirm.\"\n                    ))\n                };\n\n                Text::from(vec![text])\n            } else {\n                Text::from(vec![\n                    \"Could not find process to kill.\".into(),\n                    \"Please press ENTER or ESC to close this dialog.\".into(),\n                ])\n            }\n        };\n\n        let text: Paragraph<'_> = Paragraph::new(text)\n            .style(styles.text_style)\n            .alignment(Alignment::Center)\n            .wrap(Wrap { trim: true });\n\n        let title = match button_state {\n            #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n            ButtonState::Signals { .. } => {\n                Line::styled(\" Select Signal \", styles.widget_title_style)\n            }\n            ButtonState::Simple { .. } => {\n                Line::styled(\" Confirm Kill Process \", styles.widget_title_style)\n            }\n        };\n\n        let block = dialog_block(styles.border_type)\n            .title_top(title)\n            .title_top(Line::styled(\" Esc to close \", styles.widget_title_style).right_aligned())\n            .style(styles.border_style)\n            .border_style(styles.border_style);\n\n        let num_lines = text.line_count(block.inner(draw_area).width) as u16;\n\n        match button_state {\n            #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n            ButtonState::Signals {\n                state,\n                last_button_draw_area,\n            } => {\n                use tui::widgets::List;\n\n                // A list of options, displayed vertically.\n                const SIGNAL_TEXT_LEN: u16 = SIGNAL_TEXT.len() as u16;\n\n                // Make the rect only as big as it needs to be, which is the height of the text,\n                // the buttons, and up to 2 spaces (margin and space between), and the size of the block.\n                let [draw_area] =\n                    Layout::vertical([Constraint::Max(num_lines + SIGNAL_TEXT_LEN + 2 + 3)])\n                        .flex(Flex::Center)\n                        .areas(draw_area);\n\n                // Now we need to divide the block into one area for the paragraph,\n                // and one for the buttons.\n                let [text_draw_area, button_draw_area] = Layout::vertical([\n                    Constraint::Max(num_lines),\n                    Constraint::Max(SIGNAL_TEXT_LEN),\n                ])\n                .flex(Flex::SpaceEvenly)\n                .areas(block.inner(draw_area));\n\n                // Render the block.\n                f.render_widget(block, draw_area);\n\n                // Now render the text.\n                f.render_widget(text, text_draw_area);\n\n                // And the tricky part, rendering the buttons.\n                let selected = state\n                    .selected()\n                    .expect(\"the list state should always be initialized with a selection!\");\n\n                let buttons = List::new(SIGNAL_TEXT.iter().enumerate().map(|(index, &signal)| {\n                    let style = if index == selected {\n                        styles.selected_text_style\n                    } else {\n                        styles.text_style\n                    };\n\n                    Span::styled(signal, style)\n                }));\n\n                // This is kinda dumb how you have to set the constraint, but ok.\n                const LONGEST_SIGNAL_TEXT_LENGTH: u16 = const {\n                    let mut i = 0;\n                    let mut max = 0;\n                    while i < SIGNAL_TEXT.len() {\n                        if SIGNAL_TEXT[i].len() > max {\n                            max = SIGNAL_TEXT[i].len();\n                        }\n                        i += 1;\n                    }\n\n                    max as u16\n                };\n                let [button_draw_area] =\n                    Layout::horizontal([Constraint::Length(LONGEST_SIGNAL_TEXT_LENGTH)])\n                        .flex(Flex::Center)\n                        .areas(button_draw_area);\n\n                *last_button_draw_area = button_draw_area;\n                f.render_stateful_widget(buttons, button_draw_area, state);\n            }\n            ButtonState::Simple {\n                yes,\n                last_yes_button_area,\n                last_no_button_area,\n            } => {\n                // Make the rect only as big as it needs to be, which is the height of the text,\n                // the buttons, and up to 3 spaces (margin and space between) + 2 for block.\n                let [draw_area] = Layout::vertical([Constraint::Max(num_lines + 1 + 3 + 2)])\n                    .flex(Flex::Center)\n                    .areas(draw_area);\n\n                // Now we need to divide the block into one area for the paragraph,\n                // and one for the buttons.\n                let [text_area, button_area] =\n                    Layout::vertical([Constraint::Max(num_lines), Constraint::Length(1)])\n                        .flex(Flex::SpaceEvenly)\n                        .areas(block.inner(draw_area));\n\n                // Render things, starting from the block.\n                f.render_widget(block, draw_area);\n                f.render_widget(text, text_area);\n\n                let (yes, no) = {\n                    let (yes_style, no_style) = if *yes {\n                        (styles.selected_text_style, styles.text_style)\n                    } else {\n                        (styles.text_style, styles.selected_text_style)\n                    };\n\n                    (\n                        Paragraph::new(Span::styled(\"Yes\", yes_style)),\n                        Paragraph::new(Span::styled(\"No\", no_style)),\n                    )\n                };\n\n                let [yes_area, no_area] = Layout::horizontal([Constraint::Length(3); 2])\n                    .flex(Flex::SpaceEvenly)\n                    .areas(button_area);\n\n                *last_yes_button_area = yes_area;\n                *last_no_button_area = no_area;\n\n                f.render_widget(yes, yes_area);\n                f.render_widget(no, no_area);\n            }\n        }\n    }\n\n    #[inline]\n    fn draw_no_button_dialog(\n        &self, f: &mut Frame<'_>, draw_area: Rect, styles: &Styles, text: Text<'_>, title: Line<'_>,\n    ) {\n        let text = Paragraph::new(text)\n            .style(styles.text_style)\n            .alignment(Alignment::Center)\n            .wrap(Wrap { trim: true });\n\n        let block = dialog_block(styles.border_type)\n            .title_top(title)\n            .title_top(Line::styled(\" Esc to close \", styles.widget_title_style).right_aligned())\n            .style(styles.border_style)\n            .border_style(styles.border_style);\n\n        let num_lines = text.line_count(block.inner(draw_area).width) as u16;\n\n        // Also calculate how big of a draw loc we actually need. For this\n        // one, we want it to be shorter if possible.\n        //\n        // Note the +2 is for the margin, and another +2 for border.\n        let [draw_area] = Layout::vertical([Constraint::Max(num_lines + 2 + 2)])\n            .flex(Flex::Center)\n            .areas(draw_area);\n\n        let [text_draw_area] = Layout::vertical([Constraint::Length(num_lines)])\n            .flex(Flex::Center)\n            .areas(block.inner(draw_area));\n\n        f.render_widget(block, draw_area);\n        f.render_widget(text, text_draw_area);\n    }\n\n    /// Draw the [`ProcessKillDialog`].\n    pub fn draw(&mut self, f: &mut Frame<'_>, draw_area: Rect, styles: &Styles) {\n        // The idea is:\n        // - Use as big of a dialog box as needed (within the maximal draw loc)\n        //  - So the non-button ones are going to be smaller... probably\n        //    whatever the height of the text is.\n        //  - Meanwhile for the button one, it'll likely be full height if it's\n        //    \"advanced\" kill.\n\n        const MAX_DIALOG_WIDTH: u16 = 100;\n        let [draw_area] = Layout::horizontal([Constraint::Max(MAX_DIALOG_WIDTH)])\n            .flex(Flex::Center)\n            .areas(draw_area);\n\n        // FIXME: Add some colour to this!\n        match &mut self.state {\n            ProcessKillDialogState::NotEnabled => {}\n            ProcessKillDialogState::Selecting(state) => {\n                // Draw a text box. If buttons are yes/no, fit it, otherwise, use max space.\n                Self::draw_selecting(f, draw_area, styles, state);\n            }\n            ProcessKillDialogState::Error {\n                process_name,\n                pid,\n                err,\n            } => {\n                let text = Text::from(vec![\n                    if let Some(pid) = pid {\n                        format!(\"Failed to kill process {process_name} ({pid}):\").into()\n                    } else {\n                        format!(\"Failed to kill process '{process_name}':\").into()\n                    },\n                    err.to_owned().into(),\n                    \"Please press ENTER or ESC to close this dialog.\".into(),\n                ])\n                .alignment(Alignment::Center);\n                let title = Line::styled(\" Error \", styles.widget_title_style);\n\n                self.draw_no_button_dialog(f, draw_area, styles, text, title);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/drawing_utils.rs",
    "content": "use std::time::Instant;\n\nuse tui::{\n    layout::Rect,\n    widgets::{Block, BorderType, Borders},\n};\n\npub const SIDE_BORDERS: Borders = Borders::LEFT.union(Borders::RIGHT);\npub const AUTOHIDE_TIMEOUT_MILLISECONDS: u64 = 5000; // 5 seconds to autohide\n\n/// Determine whether a graph x-label should be hidden.\npub fn should_hide_x_label(\n    always_hide_time: bool, autohide_time: bool, timer: &mut Option<Instant>, draw_loc: Rect,\n) -> bool {\n    const TIME_LABEL_HEIGHT_LIMIT: u16 = 7;\n\n    if always_hide_time || (autohide_time && timer.is_none()) {\n        true\n    } else if let Some(time) = timer {\n        if Instant::now().duration_since(*time).as_millis() < AUTOHIDE_TIMEOUT_MILLISECONDS.into() {\n            false\n        } else {\n            *timer = None;\n            true\n        }\n    } else {\n        draw_loc.height < TIME_LABEL_HEIGHT_LIMIT\n    }\n}\n\n/// Return a widget block.\npub fn widget_block(is_basic: bool, is_selected: bool, border_type: BorderType) -> Block<'static> {\n    let mut block = Block::default().border_type(border_type);\n\n    if is_basic {\n        if is_selected {\n            block = block.borders(SIDE_BORDERS);\n        } else {\n            block = block.borders(Borders::empty());\n        }\n    } else {\n        block = block.borders(Borders::all());\n    }\n\n    block\n}\n\n/// Return a dialog block.\npub fn dialog_block(border_type: BorderType) -> Block<'static> {\n    Block::default()\n        .border_type(border_type)\n        .borders(Borders::all())\n}\n\n#[cfg(test)]\nmod test {\n\n    use super::*;\n\n    #[test]\n    fn test_should_hide_x_label() {\n        use std::time::{Duration, Instant};\n\n        use tui::layout::Rect;\n\n        let rect = Rect::new(0, 0, 10, 10);\n        let small_rect = Rect::new(0, 0, 10, 6);\n\n        let mut under_timer = Some(Instant::now());\n        let mut over_timer =\n            Instant::now().checked_sub(Duration::from_millis(AUTOHIDE_TIMEOUT_MILLISECONDS + 100));\n\n        assert!(should_hide_x_label(true, false, &mut None, rect));\n        assert!(should_hide_x_label(false, true, &mut None, rect));\n        assert!(should_hide_x_label(false, false, &mut None, small_rect));\n\n        assert!(!should_hide_x_label(\n            false,\n            true,\n            &mut under_timer,\n            small_rect\n        ));\n        assert!(under_timer.is_some());\n\n        assert!(should_hide_x_label(\n            false,\n            true,\n            &mut over_timer,\n            small_rect\n        ));\n        assert!(over_timer.is_none());\n    }\n\n    /// This test exists because previously, [`SIDE_BORDERS`] was set\n    /// incorrectly after I moved from tui-rs to ratatui.\n    #[test]\n    fn assert_side_border_bits_match() {\n        assert_eq!(\n            SIDE_BORDERS,\n            Borders::ALL.difference(Borders::TOP.union(Borders::BOTTOM))\n        )\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/battery_display.rs",
    "content": "use std::cmp::min;\n\nuse tui::{\n    Frame,\n    layout::{Constraint, Direction, Layout, Rect},\n    text::{Line, Span},\n    widgets::{Cell, Paragraph, Row, Table, Tabs},\n};\nuse unicode_width::UnicodeWidthStr;\n\nuse crate::{\n    app::App,\n    canvas::{Painter, drawing_utils::widget_block},\n    collection::batteries::BatteryState,\n    constants::*,\n};\n\n/// Calculate how many bars are to be drawn within basic mode's components.\nfn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {\n    min(\n        (num_bars_available as f64 * use_percentage / 100.0).round() as usize,\n        num_bars_available,\n    )\n}\n\nimpl Painter {\n    pub fn draw_battery(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        let should_get_widget_bounds = app_state.should_get_widget_bounds();\n        if let Some(battery_widget_state) = app_state\n            .states\n            .battery_state\n            .widget_states\n            .get_mut(&widget_id)\n        {\n            let is_selected = widget_id == app_state.current_widget.widget_id;\n            let border_style = if is_selected {\n                self.styles.highlighted_border_style\n            } else {\n                self.styles.border_style\n            };\n            let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {\n                0\n            } else {\n                app_state.app_config_fields.table_gap\n            };\n\n            let block = {\n                let mut block = widget_block(\n                    app_state.app_config_fields.use_basic_mode,\n                    is_selected,\n                    self.styles.border_type,\n                )\n                .border_style(border_style)\n                .title_top(Line::styled(\" Battery \", self.styles.widget_title_style));\n\n                if app_state.is_expanded {\n                    block = block.title_top(\n                        Line::styled(\" Esc to go back \", self.styles.widget_title_style)\n                            .right_aligned(),\n                    )\n                }\n\n                block\n            };\n\n            let battery_harvest = &(app_state.data_store.get_data().battery_harvest);\n            if battery_harvest.len() > 1 {\n                let battery_names = battery_harvest\n                    .iter()\n                    .enumerate()\n                    .map(|(itx, _)| format!(\"Battery {itx}\"))\n                    .collect::<Vec<_>>();\n\n                let tab_draw_loc = Layout::default()\n                    .constraints([\n                        Constraint::Length(1),\n                        Constraint::Length(2),\n                        Constraint::Min(0),\n                    ])\n                    .direction(Direction::Vertical)\n                    .split(draw_loc)[1];\n\n                f.render_widget(\n                    Tabs::new(\n                        battery_names\n                            .iter()\n                            .map(|name| Line::from((*name).clone()))\n                            .collect::<Vec<_>>(),\n                    )\n                    .divider(tui::symbols::line::VERTICAL)\n                    .style(self.styles.text_style)\n                    .highlight_style(self.styles.selected_text_style)\n                    .select(battery_widget_state.currently_selected_battery_index),\n                    tab_draw_loc,\n                );\n\n                if should_get_widget_bounds {\n                    let mut current_x = tab_draw_loc.x;\n                    let current_y = tab_draw_loc.y;\n                    let mut tab_click_locs: Vec<((u16, u16), (u16, u16))> = vec![];\n                    for battery in battery_names {\n                        // +1 because there's a space after the tab label.\n                        let width = UnicodeWidthStr::width(battery.as_str()) as u16;\n                        tab_click_locs\n                            .push(((current_x, current_y), (current_x + width, current_y)));\n\n                        // +4 because we want to go one space, then one space past to get to the\n                        // '|', then 2 more to start at the blank space\n                        // before the tab label.\n                        current_x += width + 4;\n                    }\n                    battery_widget_state.tab_click_locs = Some(tab_click_locs);\n                }\n            }\n\n            let is_basic = app_state.app_config_fields.use_basic_mode;\n\n            let [margined_draw_loc] = Layout::default()\n                .constraints([Constraint::Percentage(100)])\n                .horizontal_margin(u16::from(is_basic && !is_selected))\n                .direction(Direction::Horizontal)\n                .areas(draw_loc);\n\n            if let Some(battery_details) =\n                battery_harvest.get(battery_widget_state.currently_selected_battery_index)\n            {\n                let full_width = draw_loc.width.saturating_sub(2);\n                let bar_length = usize::from(full_width.saturating_sub(6));\n                let charge_percent = battery_details.charge_percent;\n\n                let num_bars = calculate_basic_use_bars(charge_percent, bar_length);\n                let bars = format!(\n                    \"[{}{}{:3.0}%]\",\n                    \"|\".repeat(num_bars),\n                    \" \".repeat(bar_length - num_bars),\n                    charge_percent,\n                );\n\n                let mut battery_charge_rows = Vec::with_capacity(2);\n                battery_charge_rows.push(Row::new([\n                    Cell::from(\"Charge\").style(self.styles.text_style)\n                ]));\n                battery_charge_rows.push(Row::new([Cell::from(bars).style(\n                    if charge_percent < 10.0 {\n                        self.styles.low_battery\n                    } else if charge_percent < 50.0 {\n                        self.styles.medium_battery\n                    } else {\n                        self.styles.high_battery\n                    },\n                )]));\n\n                let mut battery_rows = Vec::with_capacity(3);\n                let watt_consumption = battery_details.watt_consumption();\n                let health = battery_details.health();\n\n                battery_rows.push(Row::new([\"\"]).bottom_margin(table_gap + 1));\n                battery_rows\n                    .push(Row::new([\"Rate\", &watt_consumption]).style(self.styles.text_style));\n\n                battery_rows.push(\n                    Row::new([\"State\", battery_details.state.as_str()])\n                        .style(self.styles.text_style),\n                );\n\n                let mut time: String; // Keep string lifetime in scope.\n                {\n                    let style = self.styles.text_style;\n                    let time_width = (full_width / 2) as usize;\n\n                    match &battery_details.state {\n                        BatteryState::Charging {\n                            time_to_full: Some(secs),\n                        } => {\n                            time = long_time(*secs);\n\n                            if time_width >= time.len() {\n                                battery_rows.push(Row::new([\"Time to full\", &time]).style(style));\n                            } else {\n                                time = short_time(*secs);\n                                battery_rows.push(Row::new([\"To full\", &time]).style(style));\n                            }\n                        }\n                        BatteryState::Discharging {\n                            time_to_empty: Some(secs),\n                        } => {\n                            time = long_time(*secs);\n\n                            if time_width >= time.len() {\n                                battery_rows.push(Row::new([\"Time to empty\", &time]).style(style));\n                            } else {\n                                time = short_time(*secs);\n                                battery_rows.push(Row::new([\"To empty\", &time]).style(style));\n                            }\n                        }\n                        _ => {}\n                    }\n                }\n\n                battery_rows.push(Row::new([\"Health\", &health]).style(self.styles.text_style));\n\n                let header = if battery_harvest.len() > 1 {\n                    Row::new([\"\"]).bottom_margin(table_gap)\n                } else {\n                    Row::default()\n                };\n\n                // Draw bar\n                f.render_widget(\n                    Table::new(battery_charge_rows, [Constraint::Percentage(100)])\n                        .block(block.clone())\n                        .header(header.clone()),\n                    margined_draw_loc,\n                );\n\n                // Draw info\n                f.render_widget(\n                    Table::new(\n                        battery_rows,\n                        [Constraint::Percentage(50), Constraint::Percentage(50)],\n                    )\n                    .block(block)\n                    .header(header),\n                    margined_draw_loc,\n                );\n            } else {\n                let mut contents = vec![Line::default(); table_gap.into()];\n\n                contents.push(Line::from(Span::styled(\n                    \"No data found for this battery\",\n                    self.styles.text_style,\n                )));\n\n                f.render_widget(Paragraph::new(contents).block(block), margined_draw_loc);\n            }\n\n            if should_get_widget_bounds {\n                // Update draw loc in widget map\n                if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {\n                    widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));\n                    widget.bottom_right_corner = Some((\n                        margined_draw_loc.x + margined_draw_loc.width,\n                        margined_draw_loc.y + margined_draw_loc.height,\n                    ));\n                }\n            }\n        }\n    }\n}\n\nfn get_hms(secs: u32) -> (u32, u32, u32) {\n    let hours = secs / (60 * 60);\n    let minutes = (secs / 60) - hours * 60;\n    let seconds = secs - minutes * 60 - hours * 60 * 60;\n\n    (hours, minutes, seconds)\n}\n\nfn long_time(secs: u32) -> String {\n    let (hours, minutes, seconds) = get_hms(secs);\n\n    if hours > 0 {\n        let h = if hours == 1 { \"hour\" } else { \"hours\" };\n        let m = if minutes == 1 { \"minute\" } else { \"minutes\" };\n        let s = if seconds == 1 { \"second\" } else { \"seconds\" };\n\n        format!(\"{hours} {h}, {minutes} {m}, {seconds} {s}\")\n    } else {\n        let m = if minutes == 1 { \"minute\" } else { \"minutes\" };\n        let s = if seconds == 1 { \"second\" } else { \"seconds\" };\n\n        format!(\"{minutes} {m}, {seconds} {s}\")\n    }\n}\n\nfn short_time(secs: u32) -> String {\n    let (hours, minutes, seconds) = get_hms(secs);\n\n    if hours > 0 {\n        format!(\"{hours}h {minutes}m {seconds}s\")\n    } else {\n        format!(\"{minutes}m {seconds}s\")\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_get_hms() {\n        assert_eq!(get_hms(10), (0, 0, 10));\n        assert_eq!(get_hms(60), (0, 1, 0));\n        assert_eq!(get_hms(61), (0, 1, 1));\n        assert_eq!(get_hms(3600), (1, 0, 0));\n        assert_eq!(get_hms(3601), (1, 0, 1));\n        assert_eq!(get_hms(3661), (1, 1, 1));\n    }\n\n    #[test]\n    fn test_long_time() {\n        assert_eq!(long_time(1), \"0 minutes, 1 second\".to_string());\n        assert_eq!(long_time(10), \"0 minutes, 10 seconds\".to_string());\n        assert_eq!(long_time(60), \"1 minute, 0 seconds\".to_string());\n        assert_eq!(long_time(61), \"1 minute, 1 second\".to_string());\n        assert_eq!(long_time(3600), \"1 hour, 0 minutes, 0 seconds\".to_string());\n        assert_eq!(long_time(3601), \"1 hour, 0 minutes, 1 second\".to_string());\n        assert_eq!(long_time(3661), \"1 hour, 1 minute, 1 second\".to_string());\n    }\n\n    #[test]\n    fn test_short_time() {\n        assert_eq!(short_time(1), \"0m 1s\".to_string());\n        assert_eq!(short_time(10), \"0m 10s\".to_string());\n        assert_eq!(short_time(60), \"1m 0s\".to_string());\n        assert_eq!(short_time(61), \"1m 1s\".to_string());\n        assert_eq!(short_time(3600), \"1h 0m 0s\".to_string());\n        assert_eq!(short_time(3601), \"1h 0m 1s\".to_string());\n        assert_eq!(short_time(3661), \"1h 1m 1s\".to_string());\n    }\n\n    #[test]\n    fn test_calculate_basic_use_bars() {\n        // Testing various breakpoints and edge cases.\n        assert_eq!(calculate_basic_use_bars(0.0, 15), 0);\n        assert_eq!(calculate_basic_use_bars(1.0, 15), 0);\n        assert_eq!(calculate_basic_use_bars(5.0, 15), 1);\n        assert_eq!(calculate_basic_use_bars(10.0, 15), 2);\n        assert_eq!(calculate_basic_use_bars(40.0, 15), 6);\n        assert_eq!(calculate_basic_use_bars(45.0, 15), 7);\n        assert_eq!(calculate_basic_use_bars(50.0, 15), 8);\n        assert_eq!(calculate_basic_use_bars(100.0, 15), 15);\n        assert_eq!(calculate_basic_use_bars(150.0, 15), 15);\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/cpu_basic.rs",
    "content": "use std::cmp::min;\n\nuse itertools::Itertools;\nuse tui::{\n    Frame,\n    layout::{Constraint, Direction, Layout, Rect},\n};\n\nuse crate::{\n    app::App,\n    canvas::{\n        Painter,\n        components::pipe_gauge::{LabelLimit, PipeGauge},\n        drawing_utils::widget_block,\n    },\n    collection::cpu::{CpuData, CpuDataType},\n};\n\nimpl Painter {\n    /// Inspired by htop.\n    pub fn draw_basic_cpu(\n        &self, f: &mut Frame<'_>, app_state: &mut App, mut draw_loc: Rect, widget_id: u64,\n    ) {\n        let cpu_data = &app_state.data_store.get_data().cpu_harvest;\n\n        // This is a bit complicated, but basically, we want to draw SOME number\n        // of columns to draw all CPUs. Ideally, as well, we want to not have\n        // to ever scroll.\n        //\n        // **General logic** - count number of elements in cpu_data.  Then see how\n        // many rows and columns we have in draw_loc (-2 on both sides for border?).\n        // I think what we can do is try to fit in as many in one column as possible.\n        // If not, then add a new column. Then, from this, split the row space across ALL columns.\n        // From there, generate the desired lengths.\n\n        if app_state.current_widget.widget_id == widget_id {\n            f.render_widget(\n                widget_block(true, true, self.styles.border_type)\n                    .border_style(self.styles.highlighted_border_style),\n                draw_loc,\n            );\n        }\n\n        // TODO: This is pretty ugly. Is there a better way of doing it?\n        let mut avg_index = cpu_data.len() + 1;\n        let mut avg_row_count = 0;\n        if app_state.app_config_fields.dedicated_average_row\n            && app_state.app_config_fields.show_average_cpu\n        {\n            if let Some((index, avg)) = cpu_data\n                .iter()\n                .find_position(|&datum| matches!(datum.data_type, CpuDataType::Avg))\n            {\n                let (outer, inner, ratio, style) = self.cpu_info(avg);\n                let [cores_loc, mut avg_loc] =\n                    Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).areas(draw_loc);\n\n                // The cores section all have horizontal margin, so to line up with the cores we\n                // need to add some margin ourselves.\n                avg_loc.x += 1;\n                avg_loc.width -= 2;\n\n                f.render_widget(\n                    PipeGauge::default()\n                        .gauge_style(style)\n                        .label_style(style)\n                        .inner_label(inner)\n                        .start_label(outer)\n                        .ratio(ratio.into()),\n                    avg_loc,\n                );\n                avg_row_count += 1;\n                avg_index = index;\n                draw_loc = cores_loc;\n            }\n        }\n\n        if draw_loc.height > 0 {\n            let remaining_height = usize::from(draw_loc.height);\n            const REQUIRED_COLUMNS: usize = 4;\n\n            let col_constraints =\n                vec![Constraint::Percentage((100 / REQUIRED_COLUMNS) as u16); REQUIRED_COLUMNS];\n            let columns = Layout::default()\n                .constraints(col_constraints)\n                .direction(Direction::Horizontal)\n                .split(draw_loc);\n\n            let mut gauge_info = cpu_data.iter().enumerate().filter_map(|(index, cpu)| {\n                if index == avg_index {\n                    None\n                } else {\n                    Some(self.cpu_info(cpu))\n                }\n            });\n\n            // Very ugly way to sync the gauge limit across all gauges.\n            let hide_parts = columns\n                .first()\n                .map(|col| {\n                    if col.width >= 12 {\n                        LabelLimit::None\n                    } else if col.width >= 10 {\n                        LabelLimit::Bars\n                    } else {\n                        LabelLimit::StartLabel\n                    }\n                })\n                .unwrap_or_default();\n\n            let num_entries = cpu_data.len() - avg_row_count;\n            let mut row_counter = num_entries;\n            for (itx, column) in columns.iter().enumerate() {\n                if REQUIRED_COLUMNS > itx {\n                    let to_divide = REQUIRED_COLUMNS - itx;\n                    let num_taken = min(\n                        remaining_height,\n                        (row_counter / to_divide) + usize::from(row_counter % to_divide != 0),\n                    );\n                    row_counter -= num_taken;\n                    let chunk = (&mut gauge_info).take(num_taken);\n\n                    let rows = Layout::default()\n                        .direction(Direction::Vertical)\n                        .constraints(vec![Constraint::Length(1); remaining_height])\n                        .horizontal_margin(1)\n                        .split(*column);\n\n                    for ((start_label, inner_label, ratio, style), row) in chunk.zip(rows.iter()) {\n                        f.render_widget(\n                            PipeGauge::default()\n                                .gauge_style(style)\n                                .label_style(style)\n                                .inner_label(inner_label)\n                                .start_label(start_label)\n                                .ratio(ratio.into())\n                                .hide_parts(hide_parts),\n                            *row,\n                        );\n                    }\n                }\n            }\n        }\n\n        if app_state.should_get_widget_bounds() {\n            // Update draw loc in widget map\n            if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {\n                widget.top_left_corner = Some((draw_loc.x, draw_loc.y));\n                widget.bottom_right_corner =\n                    Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));\n            }\n        }\n    }\n\n    #[inline]\n    fn cpu_info(&self, data: &CpuData) -> (String, String, f32, tui::style::Style) {\n        let (outer, style) = match data.data_type {\n            CpuDataType::Avg => (\"AVG\".to_string(), self.styles.avg_cpu_colour),\n            CpuDataType::Cpu(index) => (\n                format!(\"{index:<3}\",),\n                self.styles.cpu_colour_styles[index % self.styles.cpu_colour_styles.len()],\n            ),\n        };\n\n        let inner = format!(\"{:>3.0}%\", data.usage.round());\n        let ratio = data.usage / 100.0;\n\n        (outer, inner, ratio, style)\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/cpu_graph.rs",
    "content": "use tui::{\n    Frame,\n    layout::{Constraint, Direction, Layout, Rect},\n};\n\nuse crate::{\n    app::{App, data::StoredData, layout_manager::WidgetDirection},\n    canvas::{\n        Painter,\n        components::{\n            data_table::{DrawInfo, SelectionState},\n            time_graph::{GraphData, PercentTimeGraph},\n        },\n        drawing_utils::should_hide_x_label,\n    },\n    collection::cpu::CpuData,\n    widgets::CpuWidgetState,\n};\n\nconst AVG_POSITION: usize = 1;\nconst ALL_POSITION: usize = 0;\n\nimpl Painter {\n    pub fn draw_cpu(&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64) {\n        let legend_width = (draw_loc.width as f64 * 0.15) as u16;\n\n        if legend_width < 6 {\n            // Skip drawing legend\n            if app_state.current_widget.widget_id == (widget_id + 1) {\n                if app_state.app_config_fields.cpu_left_legend {\n                    app_state.move_widget_selection(&WidgetDirection::Right);\n                } else {\n                    app_state.move_widget_selection(&WidgetDirection::Left);\n                }\n            }\n            self.draw_cpu_graph(f, app_state, draw_loc, widget_id);\n            if let Some(cpu_widget_state) =\n                app_state.states.cpu_state.widget_states.get_mut(&widget_id)\n            {\n                cpu_widget_state.is_legend_hidden = true;\n            }\n\n            // Update draw loc in widget map\n            if app_state.should_get_widget_bounds() {\n                if let Some(bottom_widget) = app_state.widget_map.get_mut(&widget_id) {\n                    bottom_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));\n                    bottom_widget.bottom_right_corner =\n                        Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));\n                }\n            }\n        } else {\n            let graph_width = draw_loc.width - legend_width;\n            let (graph_index, legend_index, constraints) =\n                if app_state.app_config_fields.cpu_left_legend {\n                    (\n                        1,\n                        0,\n                        [\n                            Constraint::Length(legend_width),\n                            Constraint::Length(graph_width),\n                        ],\n                    )\n                } else {\n                    (\n                        0,\n                        1,\n                        [\n                            Constraint::Length(graph_width),\n                            Constraint::Length(legend_width),\n                        ],\n                    )\n                };\n\n            let partitioned_draw_loc = Layout::default()\n                .margin(0)\n                .direction(Direction::Horizontal)\n                .constraints(constraints)\n                .split(draw_loc);\n\n            self.draw_cpu_graph(f, app_state, partitioned_draw_loc[graph_index], widget_id);\n            self.draw_cpu_legend(\n                f,\n                app_state,\n                partitioned_draw_loc[legend_index],\n                widget_id + 1,\n            );\n\n            if app_state.should_get_widget_bounds() {\n                // Update draw loc in widget map\n                if let Some(cpu_widget) = app_state.widget_map.get_mut(&widget_id) {\n                    cpu_widget.top_left_corner = Some((\n                        partitioned_draw_loc[graph_index].x,\n                        partitioned_draw_loc[graph_index].y,\n                    ));\n                    cpu_widget.bottom_right_corner = Some((\n                        partitioned_draw_loc[graph_index].x\n                            + partitioned_draw_loc[graph_index].width,\n                        partitioned_draw_loc[graph_index].y\n                            + partitioned_draw_loc[graph_index].height,\n                    ));\n                }\n\n                if let Some(legend_widget) = app_state.widget_map.get_mut(&(widget_id + 1)) {\n                    legend_widget.top_left_corner = Some((\n                        partitioned_draw_loc[legend_index].x,\n                        partitioned_draw_loc[legend_index].y,\n                    ));\n                    legend_widget.bottom_right_corner = Some((\n                        partitioned_draw_loc[legend_index].x\n                            + partitioned_draw_loc[legend_index].width,\n                        partitioned_draw_loc[legend_index].y\n                            + partitioned_draw_loc[legend_index].height,\n                    ));\n                }\n            }\n        }\n    }\n\n    fn generate_points<'a>(\n        &self, cpu_widget_state: &'a CpuWidgetState, data: &'a StoredData, show_avg_cpu: bool,\n    ) -> Vec<GraphData<'a>> {\n        let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 };\n        let current_scroll_position = cpu_widget_state.table.state.current_index;\n        let cpu_entries = &data.cpu_harvest;\n        let cpu_points = &data.timeseries_data.cpu;\n        let time = &data.timeseries_data.time;\n\n        if current_scroll_position == ALL_POSITION {\n            // This case ensures the other cases cannot have the position be equal to 0.\n\n            cpu_points\n                .iter()\n                .enumerate()\n                .map(|(itx, values)| {\n                    let style = if show_avg_cpu && itx == 0 {\n                        self.styles.avg_cpu_colour\n                    } else {\n                        self.styles.cpu_colour_styles\n                            [(itx - show_avg_offset) % self.styles.cpu_colour_styles.len()]\n                    };\n\n                    GraphData::default().style(style).time(time).values(values)\n                })\n                .rev()\n                .collect()\n        } else if let Some(CpuData { .. }) = cpu_entries.get(current_scroll_position - 1) {\n            // We generally subtract one from current scroll position because of the all entry.\n            // TODO: Do this a bit better (e.g. we can just do if let Some(_) = cpu_points.get())\n\n            let style = if show_avg_cpu && current_scroll_position == AVG_POSITION {\n                self.styles.avg_cpu_colour\n            } else {\n                let offset_position = current_scroll_position - 1;\n                self.styles.cpu_colour_styles\n                    [(offset_position - show_avg_offset) % self.styles.cpu_colour_styles.len()]\n            };\n\n            vec![\n                GraphData::default()\n                    .style(style)\n                    .time(time)\n                    .values(&cpu_points[current_scroll_position - 1]),\n            ]\n        } else {\n            vec![]\n        }\n    }\n\n    fn draw_cpu_graph(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        if let Some(cpu_widget_state) = app_state.states.cpu_state.widget_states.get_mut(&widget_id)\n        {\n            let data = app_state.data_store.get_data();\n\n            let hide_x_labels = should_hide_x_label(\n                app_state.app_config_fields.hide_time,\n                app_state.app_config_fields.autohide_time,\n                &mut cpu_widget_state.autohide_timer,\n                draw_loc,\n            );\n\n            let graph_data = self.generate_points(\n                cpu_widget_state,\n                data,\n                app_state.app_config_fields.show_average_cpu,\n            );\n\n            // TODO: Maybe hide load avg if too long? Or maybe the CPU part.\n            let title = {\n                #[cfg(unix)]\n                {\n                    let load_avg = &data.load_avg_harvest;\n                    let load_avg_str = format!(\n                        \"─ {:.2} {:.2} {:.2} \",\n                        load_avg[0], load_avg[1], load_avg[2]\n                    );\n\n                    concat_string::concat_string!(\" CPU \", load_avg_str).into()\n                }\n                #[cfg(not(target_family = \"unix\"))]\n                {\n                    \" CPU \".into()\n                }\n            };\n\n            PercentTimeGraph {\n                display_range: cpu_widget_state.current_display_time,\n                hide_x_labels,\n                app_config_fields: &app_state.app_config_fields,\n                current_widget: app_state.current_widget.widget_id,\n                is_expanded: app_state.is_expanded,\n                title,\n                styles: &self.styles,\n                widget_id,\n                legend_position: None,\n                legend_constraints: None,\n            }\n            .build()\n            .draw(f, draw_loc, graph_data);\n        }\n    }\n\n    fn draw_cpu_legend(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        let recalculate_column_widths = app_state.should_get_widget_bounds();\n        if let Some(cpu_widget_state) = app_state\n            .states\n            .cpu_state\n            .widget_states\n            .get_mut(&(widget_id - 1))\n        {\n            // TODO: This line (and the one above, see caller) is pretty dumb but I guess\n            // needed for now. Refactor if possible!\n            cpu_widget_state.is_legend_hidden = false;\n\n            let is_on_widget = widget_id == app_state.current_widget.widget_id;\n\n            let draw_info = DrawInfo {\n                loc: draw_loc,\n                force_redraw: app_state.is_force_redraw,\n                recalculate_column_widths,\n                selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),\n            };\n\n            cpu_widget_state.table.draw(\n                f,\n                &draw_info,\n                app_state.widget_map.get_mut(&widget_id),\n                self,\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/disk_table.rs",
    "content": "use tui::{Frame, layout::Rect};\n\nuse crate::{\n    app,\n    canvas::{\n        Painter,\n        components::data_table::{DrawInfo, SelectionState},\n    },\n};\n\nimpl Painter {\n    pub fn draw_disk_table(\n        &self, f: &mut Frame<'_>, app_state: &mut app::App, draw_loc: Rect, widget_id: u64,\n    ) {\n        let recalculate_column_widths = app_state.should_get_widget_bounds();\n        if let Some(disk_widget_state) = app_state\n            .states\n            .disk_state\n            .widget_states\n            .get_mut(&widget_id)\n        {\n            let is_on_widget = app_state.current_widget.widget_id == widget_id;\n\n            let draw_info = DrawInfo {\n                loc: draw_loc,\n                force_redraw: app_state.is_force_redraw,\n                recalculate_column_widths,\n                selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),\n            };\n\n            disk_widget_state.table.draw(\n                f,\n                &draw_info,\n                app_state.widget_map.get_mut(&widget_id),\n                self,\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/mem_basic.rs",
    "content": "use std::borrow::Cow;\n\nuse tui::{\n    Frame,\n    layout::{Constraint, Direction, Layout, Rect},\n};\n\nuse crate::{\n    app::App,\n    canvas::{Painter, components::pipe_gauge::PipeGauge, drawing_utils::widget_block},\n    collection::memory::MemData,\n    get_binary_unit_and_denominator,\n};\n\n/// Convert memory info into a string representing a fraction.\n#[inline]\nfn memory_fraction_label(data: &MemData) -> Cow<'static, str> {\n    let total_bytes = data.total_bytes.get();\n    let (unit, denominator) = get_binary_unit_and_denominator(total_bytes);\n    let used = data.used_bytes as f64 / denominator;\n    let total = total_bytes as f64 / denominator;\n\n    format!(\"{used:.1}{unit}/{total:.1}{unit}\").into()\n}\n\n/// Convert memory info into a string representing a percentage.\n#[inline]\nfn memory_percentage_label(data: &MemData) -> Cow<'static, str> {\n    let total_bytes = data.total_bytes.get();\n    let percentage = data.used_bytes as f64 / total_bytes as f64 * 100.0;\n    format!(\"{percentage:3.0}%\").into()\n}\n\n#[inline]\nfn memory_label(data: &MemData, is_percentage: bool) -> Cow<'static, str> {\n    if is_percentage {\n        memory_percentage_label(data)\n    } else {\n        memory_fraction_label(data)\n    }\n}\n\nimpl Painter {\n    pub fn draw_basic_memory(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        let mut draw_widgets: Vec<PipeGauge<'_>> = Vec::new();\n\n        if app_state.current_widget.widget_id == widget_id {\n            f.render_widget(\n                widget_block(true, true, self.styles.border_type)\n                    .border_style(self.styles.highlighted_border_style),\n                draw_loc,\n            );\n        }\n\n        let data = app_state.data_store.get_data();\n\n        let (ram_percentage, ram_label) = if let Some(ram_harvest) = &data.ram_harvest {\n            (\n                ram_harvest.percentage(),\n                memory_label(ram_harvest, app_state.basic_mode_use_percent),\n            )\n        } else {\n            (\n                0.0,\n                if app_state.basic_mode_use_percent {\n                    \"0.0B/0.0B\".into()\n                } else {\n                    \"  0%\".into()\n                },\n            )\n        };\n\n        draw_widgets.push(\n            PipeGauge::default()\n                .ratio(ram_percentage / 100.0)\n                .start_label(\"RAM\")\n                .inner_label(ram_label)\n                .label_style(self.styles.ram_style)\n                .gauge_style(self.styles.ram_style),\n        );\n\n        if let Some(swap_harvest) = &data.swap_harvest {\n            let swap_percentage = swap_harvest.percentage();\n            let swap_label = memory_label(swap_harvest, app_state.basic_mode_use_percent);\n\n            draw_widgets.push(\n                PipeGauge::default()\n                    .ratio(swap_percentage / 100.0)\n                    .start_label(\"SWP\")\n                    .inner_label(swap_label)\n                    .label_style(self.styles.swap_style)\n                    .gauge_style(self.styles.swap_style),\n            );\n        }\n\n        #[cfg(not(target_os = \"windows\"))]\n        {\n            if let Some(cache_harvest) = &data.cache_harvest {\n                let cache_percentage = cache_harvest.percentage();\n                let cache_fraction_label =\n                    memory_label(cache_harvest, app_state.basic_mode_use_percent);\n\n                draw_widgets.push(\n                    PipeGauge::default()\n                        .ratio(cache_percentage / 100.0)\n                        .start_label(\"CHE\")\n                        .inner_label(cache_fraction_label)\n                        .label_style(self.styles.cache_style)\n                        .gauge_style(self.styles.cache_style),\n                );\n            }\n        }\n\n        #[cfg(feature = \"zfs\")]\n        {\n            if let Some(arc_harvest) = &data.arc_harvest {\n                let arc_percentage = arc_harvest.percentage();\n                let arc_fraction_label =\n                    memory_label(arc_harvest, app_state.basic_mode_use_percent);\n\n                draw_widgets.push(\n                    PipeGauge::default()\n                        .ratio(arc_percentage / 100.0)\n                        .start_label(\"ARC\")\n                        .inner_label(arc_fraction_label)\n                        .label_style(self.styles.arc_style)\n                        .gauge_style(self.styles.arc_style),\n                );\n            }\n        }\n\n        #[cfg(feature = \"gpu\")]\n        {\n            let gpu_styles = &self.styles.gpu_colours;\n            let mut colour_index = 0;\n\n            for (_, harvest) in data.gpu_harvest.iter() {\n                let percentage = harvest.percentage();\n                let label = memory_label(harvest, app_state.basic_mode_use_percent);\n\n                let style = {\n                    if gpu_styles.is_empty() {\n                        tui::style::Style::default()\n                    } else {\n                        let colour = gpu_styles[colour_index % gpu_styles.len()];\n                        colour_index += 1;\n\n                        colour\n                    }\n                };\n\n                draw_widgets.push(\n                    PipeGauge::default()\n                        .ratio(percentage / 100.0)\n                        .start_label(\"GPU\")\n                        .inner_label(label)\n                        .label_style(style)\n                        .gauge_style(style),\n                );\n            }\n        }\n\n        let margined_loc = Layout::default()\n            .constraints(vec![Constraint::Length(1); draw_widgets.len()])\n            .direction(Direction::Vertical)\n            .horizontal_margin(1)\n            .split(draw_loc);\n\n        draw_widgets\n            .into_iter()\n            .enumerate()\n            .for_each(|(index, widget)| {\n                f.render_widget(widget, margined_loc[index]);\n            });\n\n        // Update draw loc in widget map\n        if app_state.should_get_widget_bounds() {\n            if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {\n                widget.top_left_corner = Some((draw_loc.x, draw_loc.y));\n                widget.bottom_right_corner =\n                    Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/mem_graph.rs",
    "content": "use std::time::Instant;\n\nuse tui::{\n    Frame,\n    layout::{Constraint, Rect},\n    style::Style,\n};\n\nuse crate::{\n    app::{App, data::Values},\n    canvas::{\n        Painter,\n        components::time_graph::{GraphData, PercentTimeGraph},\n        drawing_utils::should_hide_x_label,\n    },\n    collection::memory::MemData,\n    get_binary_unit_and_denominator,\n};\n\n/// Convert memory info into a combined memory label.\n#[inline]\nfn memory_legend_label(name: &str, data: Option<&MemData>) -> String {\n    if let Some(data) = data {\n        let total_bytes = data.total_bytes.get();\n        let percentage = data.used_bytes as f64 / total_bytes as f64 * 100.0;\n        let (unit, denominator) = get_binary_unit_and_denominator(total_bytes);\n        let used = data.used_bytes as f64 / denominator;\n        let total = total_bytes as f64 / denominator;\n\n        format!(\"{name}:{percentage:3.0}%   {used:.1}{unit}/{total:.1}{unit}\")\n    } else {\n        format!(\"{name}:   0%   0.0B/0.0B\")\n    }\n}\n\n/// Get graph data.\n#[inline]\nfn graph_data<'a>(\n    out: &mut Vec<GraphData<'a>>, name: &str, last_harvest: Option<&'a MemData>,\n    time: &'a [Instant], values: &'a Values, style: Style,\n) {\n    if !values.no_elements() {\n        let label = memory_legend_label(name, last_harvest).into();\n\n        out.push(\n            GraphData::default()\n                .name(label)\n                .time(time)\n                .values(values)\n                .style(style),\n        );\n    }\n}\n\nimpl Painter {\n    pub fn draw_memory_graph(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        if let Some(mem_state) = app_state.states.mem_state.widget_states.get_mut(&widget_id) {\n            let hide_x_labels = should_hide_x_label(\n                app_state.app_config_fields.hide_time,\n                app_state.app_config_fields.autohide_time,\n                &mut mem_state.autohide_timer,\n                draw_loc,\n            );\n            let graph_data = {\n                let mut size = 1;\n                let data = app_state.data_store.get_data();\n\n                // TODO: is this optimization really needed...? This just pre-allocates a vec, but it'll probably never\n                // be that big...\n\n                if data.swap_harvest.is_some() {\n                    size += 1; // add capacity for SWAP\n                }\n                #[cfg(feature = \"zfs\")]\n                {\n                    if data.arc_harvest.is_some() {\n                        size += 1; // add capacity for ARC\n                    }\n                }\n                #[cfg(feature = \"gpu\")]\n                {\n                    size += data.gpu_harvest.len(); // add row(s) for gpu\n                }\n\n                let mut points = Vec::with_capacity(size);\n                let timeseries = &data.timeseries_data;\n                let time = &timeseries.time;\n\n                // TODO: Add a \"no data\" option here/to time graph if there is no entries\n                graph_data(\n                    &mut points,\n                    \"RAM\",\n                    data.ram_harvest.as_ref(),\n                    time,\n                    &timeseries.ram,\n                    self.styles.ram_style,\n                );\n\n                graph_data(\n                    &mut points,\n                    \"SWP\",\n                    data.swap_harvest.as_ref(),\n                    time,\n                    &timeseries.swap,\n                    self.styles.swap_style,\n                );\n\n                #[cfg(not(target_os = \"windows\"))]\n                {\n                    graph_data(\n                        &mut points,\n                        \"CACHE\", // TODO: Figure out how to line this up better\n                        data.cache_harvest.as_ref(),\n                        time,\n                        &timeseries.cache_mem,\n                        self.styles.cache_style,\n                    );\n                }\n\n                #[cfg(feature = \"zfs\")]\n                {\n                    graph_data(\n                        &mut points,\n                        \"ARC\",\n                        data.arc_harvest.as_ref(),\n                        time,\n                        &timeseries.arc_mem,\n                        self.styles.arc_style,\n                    );\n                }\n\n                #[cfg(feature = \"gpu\")]\n                {\n                    let mut colour_index = 0;\n                    let gpu_styles = &self.styles.gpu_colours;\n\n                    for (name, harvest) in &data.gpu_harvest {\n                        if let Some(gpu_data) = data.timeseries_data.gpu_mem.get(name) {\n                            let style = {\n                                if gpu_styles.is_empty() {\n                                    Style::default()\n                                } else {\n                                    let colour = gpu_styles[colour_index % gpu_styles.len()];\n                                    colour_index += 1;\n\n                                    colour\n                                }\n                            };\n\n                            graph_data(\n                                &mut points,\n                                name, // TODO: REALLY figure out how to line this up better\n                                Some(harvest),\n                                time,\n                                gpu_data,\n                                style,\n                            );\n                        }\n                    }\n                }\n\n                points\n            };\n\n            PercentTimeGraph {\n                display_range: mem_state.current_display_time,\n                hide_x_labels,\n                app_config_fields: &app_state.app_config_fields,\n                current_widget: app_state.current_widget.widget_id,\n                is_expanded: app_state.is_expanded,\n                title: \" Memory \".into(),\n                styles: &self.styles,\n                widget_id,\n                legend_position: app_state.app_config_fields.memory_legend_position,\n                legend_constraints: Some((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))),\n            }\n            .build()\n            .draw(f, draw_loc, graph_data);\n        }\n\n        if app_state.should_get_widget_bounds() {\n            // Update draw loc in widget map\n            if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {\n                widget.top_left_corner = Some((draw_loc.x, draw_loc.y));\n                widget.bottom_right_corner =\n                    Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/mod.rs",
    "content": "use crate::{collection::network::NetworkHarvest, utils::data_units::convert_bytes};\n\npub mod cpu_basic;\npub mod cpu_graph;\npub mod disk_table;\npub mod mem_basic;\npub mod mem_graph;\npub mod network_basic;\npub mod network_graph;\npub mod process_table;\npub mod temperature_table;\n\n#[cfg(feature = \"battery\")]\npub mod battery_display;\n\n/// Helper struct to hold packet-related data\npub(super) struct PacketInfo {\n    /// Current received packet rate.\n    pub(super) rx_packet_rate: u64,\n\n    /// Current transmitted packet rate.\n    pub(super) tx_packet_rate: u64,\n\n    /// Average received packet size in bytes, converted to the nearest unit.\n    pub(super) avg_rx_packet_size: (f64, &'static str),\n\n    /// Average transmitted packet size in bytes, converted to the nearest unit.\n    pub(super) avg_tx_packet_size: (f64, &'static str),\n}\n\n/// Calculate packet information from network data.\npub(super) fn calculate_packet_info(\n    network_latest_data: &NetworkHarvest, use_binary_prefix: bool,\n) -> PacketInfo {\n    let rx_packet_rate = network_latest_data.rx_packets;\n    let tx_packet_rate = network_latest_data.tx_packets;\n\n    // Calculate average packet size (bytes per packet)\n    let avg_rx_packet_size = if network_latest_data.rx_packets > 0 {\n        (network_latest_data.rx as f64 / 8.0) / network_latest_data.rx_packets as f64 // Convert bits to bytes\n    } else {\n        0.0\n    };\n\n    let avg_tx_packet_size = if network_latest_data.tx_packets > 0 {\n        (network_latest_data.tx as f64 / 8.0) / network_latest_data.tx_packets as f64 // Convert bits to bytes\n    } else {\n        0.0\n    };\n\n    let avg_rx_packet_size = convert_bytes(avg_rx_packet_size.round() as u64, use_binary_prefix);\n    let avg_tx_packet_size = convert_bytes(avg_tx_packet_size.round() as u64, use_binary_prefix);\n\n    PacketInfo {\n        rx_packet_rate,\n        tx_packet_rate,\n        avg_rx_packet_size,\n        avg_tx_packet_size,\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/network_basic.rs",
    "content": "use tui::{\n    Frame,\n    layout::{Constraint, Direction, Layout, Rect},\n    text::{Line, Span},\n    widgets::{Block, Paragraph},\n};\n\nuse crate::{\n    app::App,\n    canvas::{\n        Painter,\n        drawing_utils::widget_block,\n        widgets::{PacketInfo, calculate_packet_info},\n    },\n    utils::data_units::{convert_bits, get_unit_prefix},\n};\n\nimpl Painter {\n    pub fn draw_basic_network(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        let show_packets = app_state.app_config_fields.network_show_packets;\n\n        if app_state.current_widget.widget_id == widget_id {\n            f.render_widget(\n                widget_block(true, true, self.styles.border_type)\n                    .border_style(self.styles.highlighted_border_style),\n                draw_loc,\n            );\n        }\n\n        let use_binary_prefix = app_state.app_config_fields.network_use_binary_prefix;\n        let network_data = &(app_state.data_store.get_data().network_harvest);\n        let rx = get_unit_prefix(network_data.rx, use_binary_prefix);\n        let tx = get_unit_prefix(network_data.tx, use_binary_prefix);\n        let total_rx = convert_bits(network_data.total_rx, use_binary_prefix);\n        let total_tx = convert_bits(network_data.total_tx, use_binary_prefix);\n\n        let rx_label = format!(\"RX: {:.1}{}/s\", rx.0, rx.1);\n        let tx_label = format!(\"TX: {:.1}{}/s\", tx.0, tx.1);\n        let total_rx_label = format!(\"Total RX: {:.1}{}\", total_rx.0, total_rx.1);\n        let total_tx_label = format!(\"Total TX: {:.1}{}\", total_tx.0, total_tx.1);\n\n        // Determine if we need grid layout based on available width\n        // Assume we need at least ~15 chars per column for horizontal layout\n        // With 4 columns, that's ~60 chars minimum\n        // If width is less than 60, use grid layout (4 rows x 2 columns)\n        if show_packets {\n            let PacketInfo {\n                rx_packet_rate,\n                tx_packet_rate,\n                avg_rx_packet_size,\n                avg_tx_packet_size,\n            } = calculate_packet_info(network_data, use_binary_prefix);\n\n            // TODO: Stylize packet stuff later with something else? Or maybe make it so total is now (by default) just bolded RX/TX? I doubt anyone cares...\n            let rx_packet_rate_label = format!(\"RX Pkt: {}pkt/s\", rx_packet_rate);\n            let tx_packet_rate_label = format!(\"TX Pkt: {}pkt/s\", tx_packet_rate);\n            let avg_rx_packet_size_label = format!(\n                \"Avg RX Pkt: {:.1}{}\",\n                avg_rx_packet_size.0, avg_rx_packet_size.1\n            );\n            let avg_tx_packet_size_label = format!(\n                \"Avg TX Pkt: {:.1}{}\",\n                avg_tx_packet_size.0, avg_tx_packet_size.1\n            );\n\n            if draw_loc.width < 60 {\n                // 4 rows x 2 columns layout\n                // Column 1: RX, TX, Total RX, Total TX (top to bottom)\n                // Column 2: RX Packets, TX Packets, AVG RX, AVG TX (top to bottom)\n                let grid_loc = Layout::default()\n                    .direction(Direction::Horizontal)\n                    .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])\n                    .split(draw_loc);\n\n                // Column 1: RX, TX, Total RX, Total TX\n                let col1_loc = Layout::default()\n                    .direction(Direction::Vertical)\n                    .constraints([\n                        Constraint::Percentage(25),\n                        Constraint::Percentage(25),\n                        Constraint::Percentage(25),\n                        Constraint::Percentage(25),\n                    ])\n                    .split(grid_loc[0]);\n                f.render_widget(\n                    Paragraph::new(Line::from(Span::styled(rx_label, self.styles.rx_style)))\n                        .block(Block::default()),\n                    col1_loc[0],\n                );\n                f.render_widget(\n                    Paragraph::new(Line::from(Span::styled(tx_label, self.styles.tx_style)))\n                        .block(Block::default()),\n                    col1_loc[1],\n                );\n                f.render_widget(\n                    Paragraph::new(Line::from(Span::styled(\n                        total_rx_label,\n                        self.styles.total_rx_style,\n                    )))\n                    .block(Block::default()),\n                    col1_loc[2],\n                );\n                f.render_widget(\n                    Paragraph::new(Line::from(Span::styled(\n                        total_tx_label,\n                        self.styles.total_tx_style,\n                    )))\n                    .block(Block::default()),\n                    col1_loc[3],\n                );\n\n                // Column 2: RX Packets, TX Packets, AVG RX, AVG TX\n                let col2_loc = Layout::default()\n                    .direction(Direction::Vertical)\n                    .constraints([\n                        Constraint::Percentage(25),\n                        Constraint::Percentage(25),\n                        Constraint::Percentage(25),\n                        Constraint::Percentage(25),\n                    ])\n                    .split(grid_loc[1]);\n                f.render_widget(\n                    Paragraph::new(Line::from(Span::styled(\n                        rx_packet_rate_label,\n                        self.styles.rx_style,\n                    )))\n                    .block(Block::default()),\n                    col2_loc[0],\n                );\n                f.render_widget(\n                    Paragraph::new(Line::from(Span::styled(\n                        tx_packet_rate_label,\n                        self.styles.tx_style,\n                    )))\n                    .block(Block::default()),\n                    col2_loc[1],\n                );\n                f.render_widget(\n                    Paragraph::new(Line::from(Span::styled(\n                        avg_rx_packet_size_label,\n                        self.styles.total_rx_style,\n                    )))\n                    .block(Block::default()),\n                    col2_loc[2],\n                );\n                f.render_widget(\n                    Paragraph::new(Line::from(Span::styled(\n                        avg_tx_packet_size_label,\n                        self.styles.total_tx_style,\n                    )))\n                    .block(Block::default()),\n                    col2_loc[3],\n                );\n            } else {\n                // Horizontal 4-column layout\n                let constraints = [\n                    Constraint::Percentage(25),\n                    Constraint::Percentage(25),\n                    Constraint::Percentage(25),\n                    Constraint::Percentage(25),\n                ];\n\n                let divided_loc = Layout::default()\n                    .direction(Direction::Horizontal)\n                    .constraints(constraints)\n                    .split(draw_loc);\n\n                // Column 1: RX/TX\n                let col1_loc = Layout::default()\n                    .direction(Direction::Horizontal)\n                    .constraints([Constraint::Percentage(100)])\n                    .horizontal_margin(1)\n                    .split(divided_loc[0]);\n                let col1_text = vec![\n                    Line::from(Span::styled(rx_label, self.styles.rx_style)),\n                    Line::from(Span::styled(tx_label, self.styles.tx_style)),\n                ];\n                f.render_widget(\n                    Paragraph::new(col1_text).block(Block::default()),\n                    col1_loc[0],\n                );\n\n                // Column 2: Total RX/TX\n                let col2_loc = Layout::default()\n                    .direction(Direction::Horizontal)\n                    .constraints([Constraint::Percentage(100)])\n                    .horizontal_margin(1)\n                    .split(divided_loc[1]);\n                let col2_text = vec![\n                    Line::from(Span::styled(total_rx_label, self.styles.total_rx_style)),\n                    Line::from(Span::styled(total_tx_label, self.styles.total_tx_style)),\n                ];\n                f.render_widget(\n                    Paragraph::new(col2_text).block(Block::default()),\n                    col2_loc[0],\n                );\n\n                // Column 3: RX/TX packets\n                let col3_loc = Layout::default()\n                    .direction(Direction::Horizontal)\n                    .constraints([Constraint::Percentage(100)])\n                    .horizontal_margin(1)\n                    .split(divided_loc[2]);\n                let col3_text = vec![\n                    Line::from(Span::styled(rx_packet_rate_label, self.styles.rx_style)),\n                    Line::from(Span::styled(tx_packet_rate_label, self.styles.tx_style)),\n                ];\n                f.render_widget(\n                    Paragraph::new(col3_text).block(Block::default()),\n                    col3_loc[0],\n                );\n\n                // Column 4: AVG RX/TX packets\n                let col4_loc = Layout::default()\n                    .direction(Direction::Horizontal)\n                    .constraints([Constraint::Percentage(100)])\n                    .horizontal_margin(1)\n                    .split(divided_loc[3]);\n                let col4_text = vec![\n                    Line::from(Span::styled(\n                        avg_rx_packet_size_label,\n                        self.styles.total_rx_style,\n                    )),\n                    Line::from(Span::styled(\n                        avg_tx_packet_size_label,\n                        self.styles.total_tx_style,\n                    )),\n                ];\n                f.render_widget(\n                    Paragraph::new(col4_text).block(Block::default()),\n                    col4_loc[0],\n                );\n            }\n        } else {\n            // No packets, 2-column layout\n            let constraints = [Constraint::Percentage(50), Constraint::Percentage(50)];\n\n            let divided_loc = Layout::default()\n                .direction(Direction::Horizontal)\n                .constraints(constraints)\n                .split(draw_loc);\n\n            // Column 1: RX/TX\n            let col1_loc = Layout::default()\n                .direction(Direction::Horizontal)\n                .constraints([Constraint::Percentage(100)])\n                .horizontal_margin(1)\n                .split(divided_loc[0]);\n            let col1_text = vec![\n                Line::from(Span::styled(rx_label, self.styles.rx_style)),\n                Line::from(Span::styled(tx_label, self.styles.tx_style)),\n            ];\n            f.render_widget(\n                Paragraph::new(col1_text).block(Block::default()),\n                col1_loc[0],\n            );\n\n            // Column 2: Total RX/TX\n            let col2_loc = Layout::default()\n                .direction(Direction::Horizontal)\n                .constraints([Constraint::Percentage(100)])\n                .horizontal_margin(1)\n                .split(divided_loc[1]);\n            let col2_text = vec![\n                Line::from(Span::styled(total_rx_label, self.styles.total_rx_style)),\n                Line::from(Span::styled(total_tx_label, self.styles.total_tx_style)),\n            ];\n            f.render_widget(\n                Paragraph::new(col2_text).block(Block::default()),\n                col2_loc[0],\n            );\n        }\n\n        // Update draw loc in widget map\n        if app_state.should_get_widget_bounds() {\n            if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {\n                widget.top_left_corner = Some((draw_loc.x, draw_loc.y));\n                widget.bottom_right_corner =\n                    Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/network_graph.rs",
    "content": "use std::time::Duration;\n\nuse tui::{\n    Frame,\n    layout::{Constraint, Direction, Layout, Rect},\n    symbols::Marker,\n    text::Text,\n    widgets::{Block, Borders, Row, Table},\n};\n\nuse crate::{\n    app::{App, AppConfigFields, AxisScaling},\n    canvas::{\n        Painter,\n        components::time_graph::{AxisBound, ChartScaling, GraphData, TimeGraph},\n        drawing_utils::should_hide_x_label,\n        widgets::{PacketInfo, calculate_packet_info},\n    },\n    utils::{\n        data_units::*,\n        general::{saturating_log2, saturating_log10},\n    },\n    widgets::{NetWidgetHeightCache, NetWidgetState},\n};\n\nimpl Painter {\n    pub fn draw_network(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        if app_state.app_config_fields.use_old_network_legend {\n            const LEGEND_HEIGHT: u16 = 4;\n            let network_chunk = Layout::default()\n                .direction(Direction::Vertical)\n                .margin(0)\n                .constraints([\n                    Constraint::Length(draw_loc.height.saturating_sub(LEGEND_HEIGHT)),\n                    Constraint::Length(LEGEND_HEIGHT),\n                ])\n                .split(draw_loc);\n\n            self.draw_network_graph(f, app_state, network_chunk[0], widget_id);\n            self.draw_old_network_labels(f, app_state, network_chunk[1], widget_id);\n        } else {\n            self.draw_network_graph(f, app_state, draw_loc, widget_id);\n        }\n\n        if app_state.should_get_widget_bounds() {\n            // Update draw loc in widget map\n            // Note that in both cases, we always go to the same widget id so it's fine to\n            // do it like this lol.\n            if let Some(network_widget) = app_state.widget_map.get_mut(&widget_id) {\n                network_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));\n                network_widget.bottom_right_corner =\n                    Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));\n            }\n        }\n    }\n\n    pub fn draw_network_graph(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        if let Some(network_widget_state) =\n            app_state.states.net_state.widget_states.get_mut(&widget_id)\n        {\n            let shared_data = app_state.data_store.get_data();\n            let network_latest_data = &(shared_data.network_harvest);\n            let rx_points = &(shared_data.timeseries_data.rx);\n            let tx_points = &(shared_data.timeseries_data.tx);\n            let times = &(shared_data.timeseries_data.time);\n            let time_start = -(network_widget_state.current_display_time as f64);\n\n            let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);\n            let hide_x_labels = should_hide_x_label(\n                app_state.app_config_fields.hide_time,\n                app_state.app_config_fields.autohide_time,\n                &mut network_widget_state.autohide_timer,\n                draw_loc,\n            );\n\n            let y_max = {\n                if let Some(last_time) = times.last() {\n                    let cached_network_height =\n                        check_network_height_cache(network_widget_state, last_time);\n\n                    let (mut biggest, mut biggest_time, oldest_to_check) = cached_network_height\n                        .unwrap_or_else(|| {\n                            let visible_duration =\n                                Duration::from_millis(network_widget_state.current_display_time);\n\n                            let visible_left_bound = match last_time.checked_sub(visible_duration) {\n                                Some(v) => v,\n                                None => {\n                                    // On some systems (like Windows) it can be possible that the current display time\n                                    // causes subtraction to fail if, for example, the uptime of the system is too low\n                                    // and current_display_time is too high. See https://github.com/ClementTsang/bottom/issues/1825.\n                                    //\n                                    // As such, we instead take the oldest visible time. This is a bit inefficient, but\n                                    // since it should only happen rarely, it should be fine.\n                                    times\n                                        .iter()\n                                        .take_while(|t| {\n                                            last_time.duration_since(**t) < visible_duration\n                                        })\n                                        .last()\n                                        .cloned()\n                                        .unwrap_or(*last_time)\n                                }\n                            };\n\n                            (0.0, visible_left_bound, visible_left_bound)\n                        });\n\n                    for (&time, &v) in rx_points\n                        .iter_along_base(times)\n                        .rev()\n                        .take_while(|&(&time, _)| time >= oldest_to_check)\n                    {\n                        if v > biggest {\n                            biggest = v;\n                            biggest_time = time;\n                        }\n                    }\n\n                    for (&time, &v) in tx_points\n                        .iter_along_base(times)\n                        .rev()\n                        .take_while(|&(&time, _)| time >= oldest_to_check)\n                    {\n                        if v > biggest {\n                            biggest = v;\n                            biggest_time = time;\n                        }\n                    }\n\n                    network_widget_state.height_cache = Some(NetWidgetHeightCache {\n                        best_point: (biggest_time, biggest),\n                        right_edge: *last_time,\n                        period: network_widget_state.current_display_time,\n                    });\n\n                    biggest\n                } else {\n                    0.0\n                }\n            };\n            let (adjusted_y_max, y_labels) =\n                adjust_network_data_point(y_max, &app_state.app_config_fields);\n            let y_bounds = AxisBound::Max(adjusted_y_max);\n\n            let use_old_network_legend = app_state.app_config_fields.use_old_network_legend;\n            let legend_constraints = if use_old_network_legend {\n                // Always hide it. Note that I could pass in `None` to the position as well but eh this works.\n                (Constraint::Length(0), Constraint::Length(0))\n            } else {\n                // Hide the legend if the width is 75% of the total widget width\n                // or the height is greater than 75% of the total widget hight.\n                (Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))\n            };\n\n            // TODO: Add support for clicking on legend to only show that value on chart.\n            let use_binary_prefix = app_state.app_config_fields.network_use_binary_prefix;\n            let unit_type = app_state.app_config_fields.network_unit_type;\n            let unit = match unit_type {\n                DataUnit::Byte => \"B/s\",\n                DataUnit::Bit => \"b/s\",\n            };\n\n            let rx = get_unit_prefix(network_latest_data.rx, use_binary_prefix);\n            let tx = get_unit_prefix(network_latest_data.tx, use_binary_prefix);\n            let total_rx = convert_bits(network_latest_data.total_rx, use_binary_prefix);\n            let total_tx = convert_bits(network_latest_data.total_tx, use_binary_prefix);\n\n            let graph_data = if use_old_network_legend {\n                let mut graph_data = vec![\n                    GraphData::default()\n                        .time(times)\n                        .values(rx_points)\n                        .style(self.styles.rx_style),\n                    GraphData::default()\n                        .time(times)\n                        .values(tx_points)\n                        .style(self.styles.tx_style),\n                ];\n\n                graph_data.extend(vec![\n                    GraphData::default().style(self.styles.total_rx_style),\n                    GraphData::default().style(self.styles.total_tx_style),\n                ]);\n\n                graph_data\n            } else {\n                let rx_label = format!(\"{:.1}{}{}\", rx.0, rx.1, unit);\n                let tx_label = format!(\"{:.1}{}{}\", tx.0, tx.1, unit);\n                let total_rx_label = format!(\"{:.1}{}\", total_rx.0, total_rx.1);\n                let total_tx_label = format!(\"{:.1}{}\", total_tx.0, total_tx.1);\n\n                // Add packets information if enabled and there's enough room.\n                const MAX_LEGEND_WIDTH: u16 = 70;\n                let approx_legend_width = draw_loc.width * 3 / 4;\n\n                // FIXME: I'm not really a huge fan of this - I think it may be better to just not support this and\n                // allow for more easily spawning a separate legend table (basically old legend).\n                if app_state.app_config_fields.network_show_packets\n                    && approx_legend_width > MAX_LEGEND_WIDTH\n                {\n                    let PacketInfo {\n                        rx_packet_rate,\n                        tx_packet_rate,\n                        avg_rx_packet_size,\n                        avg_tx_packet_size,\n                    } = calculate_packet_info(network_latest_data, use_binary_prefix);\n\n                    let avg_rx_packet_size_label =\n                        format!(\"{:.1}{}\", avg_rx_packet_size.0, avg_rx_packet_size.1);\n                    let avg_tx_packet_size_label =\n                        format!(\"{:.1}{}\", avg_tx_packet_size.0, avg_tx_packet_size.1);\n\n                    vec![\n                            GraphData::default()\n                                .name(format!(\"RX: {rx_label:<10} All: {total_rx_label:<8} Packets: {rx_packet_rate:>8}pkt/s Avg: {avg_rx_packet_size_label}\").into())\n                                .time(times)\n                                .values(rx_points)\n                                .style(self.styles.rx_style),\n                            GraphData::default()\n                                .name(format!(\"TX: {tx_label:<10} All: {total_tx_label:<8} Packets: {tx_packet_rate:>8}pkt/s Avg: {avg_tx_packet_size_label}\").into())\n                                .time(times)\n                                .values(tx_points)\n                                .style(self.styles.tx_style),\n                        ]\n                } else {\n                    vec![\n                        GraphData::default()\n                            .name(format!(\"RX: {rx_label:<10} All: {total_rx_label}\").into())\n                            .time(times)\n                            .values(rx_points)\n                            .style(self.styles.rx_style),\n                        GraphData::default()\n                            .name(format!(\"TX: {tx_label:<10} All: {total_tx_label}\").into())\n                            .time(times)\n                            .values(tx_points)\n                            .style(self.styles.tx_style),\n                    ]\n                }\n            };\n\n            let marker = if app_state.app_config_fields.use_dot {\n                Marker::Dot\n            } else {\n                Marker::Braille\n            };\n\n            let scaling = match app_state.app_config_fields.network_scale_type {\n                AxisScaling::Log => {\n                    // TODO: I might change this behaviour later.\n                    if app_state.app_config_fields.network_use_binary_prefix {\n                        ChartScaling::Log2\n                    } else {\n                        ChartScaling::Log10\n                    }\n                }\n                AxisScaling::Linear => ChartScaling::Linear,\n            };\n\n            TimeGraph {\n                x_min: time_start,\n                hide_x_labels,\n                y_bounds,\n                y_labels: &(y_labels.into_iter().map(Into::into).collect::<Vec<_>>()),\n                graph_style: self.styles.graph_style,\n                border_style,\n                border_type: self.styles.border_type,\n                title: \" Network \".into(),\n                is_selected: app_state.current_widget.widget_id == widget_id,\n                is_expanded: app_state.is_expanded,\n                title_style: self.styles.widget_title_style,\n                legend_position: app_state.app_config_fields.network_legend_position,\n                legend_constraints: Some(legend_constraints),\n                marker,\n                scaling,\n            }\n            .draw(f, draw_loc, graph_data);\n        }\n    }\n\n    fn draw_old_network_labels(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        let network_latest_data = &(app_state.data_store.get_data().network_harvest);\n        let use_binary_prefix = app_state.app_config_fields.network_use_binary_prefix;\n        let unit_type = app_state.app_config_fields.network_unit_type;\n        let unit = match unit_type {\n            DataUnit::Byte => \"B/s\",\n            DataUnit::Bit => \"b/s\",\n        };\n\n        let rx = get_unit_prefix(network_latest_data.rx, use_binary_prefix);\n        let tx = get_unit_prefix(network_latest_data.tx, use_binary_prefix);\n\n        let rx_label = format!(\"{:.1}{}{}\", rx.0, rx.1, unit);\n        let tx_label = format!(\"{:.1}{}{}\", tx.0, tx.1, unit);\n\n        let total_rx = convert_bits(network_latest_data.total_rx, use_binary_prefix);\n        let total_tx = convert_bits(network_latest_data.total_tx, use_binary_prefix);\n        let total_rx_label = format!(\"{:.1}{}\", total_rx.0, total_rx.1);\n        let total_tx_label = format!(\"{:.1}{}\", total_tx.0, total_tx.1);\n\n        let total_network = if app_state.app_config_fields.network_show_packets {\n            let PacketInfo {\n                rx_packet_rate,\n                tx_packet_rate,\n                avg_rx_packet_size,\n                avg_tx_packet_size,\n            } = calculate_packet_info(network_latest_data, use_binary_prefix);\n\n            let avg_rx_packet_size_label =\n                format!(\"{:.1}{}\", avg_rx_packet_size.0, avg_rx_packet_size.1);\n            let avg_tx_packet_size_label =\n                format!(\"{:.1}{}\", avg_tx_packet_size.0, avg_tx_packet_size.1);\n\n            vec![Row::new([\n                Text::styled(rx_label, self.styles.rx_style),\n                Text::styled(tx_label, self.styles.tx_style),\n                Text::styled(total_rx_label, self.styles.total_rx_style),\n                Text::styled(total_tx_label, self.styles.total_tx_style),\n                Text::styled(format!(\"{rx_packet_rate}pkt/s\"), self.styles.rx_style),\n                Text::styled(format!(\"{tx_packet_rate}pkt/s\"), self.styles.tx_style),\n                Text::styled(avg_rx_packet_size_label, self.styles.rx_style),\n                Text::styled(avg_tx_packet_size_label, self.styles.tx_style),\n            ])]\n        } else {\n            vec![Row::new([\n                Text::styled(rx_label, self.styles.rx_style),\n                Text::styled(tx_label, self.styles.tx_style),\n                Text::styled(total_rx_label, self.styles.total_rx_style),\n                Text::styled(total_tx_label, self.styles.total_tx_style),\n            ])]\n        };\n\n        let headers = if app_state.app_config_fields.network_show_packets {\n            vec![\n                \"RX\", \"TX\", \"Total RX\", \"Total TX\", \"RX Pkts\", \"TX Pkts\", \"Avg RX\", \"Avg TX\",\n            ]\n        } else {\n            vec![\"RX\", \"TX\", \"Total RX\", \"Total TX\"]\n        };\n        let num_columns = headers.len();\n\n        let column_width = draw_loc.width.saturating_sub(2) / num_columns as u16;\n\n        // Draw\n        f.render_widget(\n            Table::new(\n                total_network,\n                &((std::iter::repeat_n(column_width, num_columns))\n                    .map(Constraint::Length)\n                    .collect::<Vec<_>>()),\n            )\n            .header(Row::new(headers).style(self.styles.table_header_style))\n            .block(Block::default().borders(Borders::ALL).border_style(\n                if app_state.current_widget.widget_id == widget_id {\n                    self.styles.highlighted_border_style\n                } else {\n                    self.styles.border_style\n                },\n            ))\n            .style(self.styles.text_style),\n            draw_loc,\n        );\n    }\n}\n\n/// Returns a cached max value, it's time, and what period it covers if it is cached.\n#[inline]\nfn check_network_height_cache(\n    network_widget_state: &NetWidgetState, last_time: &std::time::Instant,\n) -> Option<(f64, std::time::Instant, std::time::Instant)> {\n    let visible_duration = Duration::from_millis(network_widget_state.current_display_time);\n\n    if let Some(NetWidgetHeightCache {\n        best_point,\n        right_edge,\n        period,\n    }) = &network_widget_state.height_cache\n    {\n        if *period == network_widget_state.current_display_time\n            && last_time.duration_since(best_point.0) < visible_duration\n        {\n            return Some((best_point.1, best_point.0, *right_edge));\n        }\n    }\n\n    None\n}\n\n/// Returns the required labels.\n///\n/// TODO: This is _really_ ugly... also there might be a bug with certain heights and too many labels.\n/// We may need to take draw height into account, either here, or in the time graph itself.\nfn adjust_network_data_point(max_entry: f64, config: &AppConfigFields) -> (f64, Vec<String>) {\n    // So, we're going with an approach like this for linear data:\n    // - Main goal is to maximize the amount of information displayed given a\n    //   specific height. We don't want to drown out some data if the ranges are too\n    //   far though!  Nor do we want to filter out too much data...\n    // - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max\n    //   load.\n    //\n    // The idea is we take the top value, build our scale such that each \"point\" is\n    // a scaled version of that. So for example, let's say I use 390 Mb/s.  If I\n    // drew 4 segments, it would be 97.5, 195, 292.5, 390, and\n    // probably something like 438.75?\n    //\n    // So, how do we do this in ratatui?  Well, if we are using intervals that tie\n    // in perfectly to the max value we want... then it's actually not that\n    // hard. Since ratatui accepts a vector as labels and will properly space\n    // them all out... we just work with that and space it out properly.\n    //\n    // Dynamic chart idea based off of FreeNAS's chart design.\n    //\n    // ---\n    //\n    // For log data, we just use the old method of log intervals (kilo/mega/giga/etc.).\n    // Keep it nice and simple.\n\n    // Now just check the largest unit we correspond to... then proceed to build\n    // some entries from there!\n\n    let scale_type = config.network_scale_type;\n    let use_binary_prefix = config.network_use_binary_prefix;\n    let network_unit_type = config.network_unit_type;\n\n    let unit_char = match network_unit_type {\n        DataUnit::Byte => \"B\",\n        DataUnit::Bit => \"b\",\n    };\n\n    match scale_type {\n        AxisScaling::Linear => {\n            let (k_limit, m_limit, g_limit, t_limit) = if use_binary_prefix {\n                (\n                    KIBI_LIMIT_F64,\n                    MEBI_LIMIT_F64,\n                    GIBI_LIMIT_F64,\n                    TEBI_LIMIT_F64,\n                )\n            } else {\n                (\n                    KILO_LIMIT_F64,\n                    MEGA_LIMIT_F64,\n                    GIGA_LIMIT_F64,\n                    TERA_LIMIT_F64,\n                )\n            };\n\n            let max_entry_upper = if max_entry == 0.0 {\n                // If it's 0, then just use a very low value so the labels aren't just \"0.0\" 4 times.\n                // This _also_ prevents the y-axis height range ever being 0.\n                1.0\n            } else {\n                max_entry * 1.5 // We use the bumped up version to calculate our unit type.\n            };\n\n            let (max_value_scaled, unit_prefix, unit_type): (f64, &str, &str) = {\n                if max_entry_upper < k_limit {\n                    (max_entry, \"\", unit_char)\n                } else if max_entry_upper < m_limit {\n                    (\n                        max_entry / k_limit,\n                        if use_binary_prefix { \"Ki\" } else { \"K\" },\n                        unit_char,\n                    )\n                } else if max_entry_upper < g_limit {\n                    (\n                        max_entry / m_limit,\n                        if use_binary_prefix { \"Mi\" } else { \"M\" },\n                        unit_char,\n                    )\n                } else if max_entry_upper < t_limit {\n                    (\n                        max_entry / g_limit,\n                        if use_binary_prefix { \"Gi\" } else { \"G\" },\n                        unit_char,\n                    )\n                } else {\n                    (\n                        max_entry / t_limit,\n                        if use_binary_prefix { \"Ti\" } else { \"T\" },\n                        unit_char,\n                    )\n                }\n            };\n\n            // Finally, build an acceptable range starting from there, using the given\n            // height! Note we try to put more of a weight on the bottom section\n            // vs. the top, since the top has less data.\n            let base_unit = max_value_scaled;\n            let labels: Vec<String> = vec![\n                format!(\"0{unit_prefix}{unit_type}\"),\n                format!(\"{:.1}\", base_unit * 0.5),\n                format!(\"{:.1}\", base_unit),\n                format!(\"{:.1}\", base_unit * 1.5),\n            ]\n            .into_iter()\n            .map(|s| {\n                // Pull 5 as the longest legend value is generally going to be 5 digits (if they somehow hit over 5 terabits per second)\n                format!(\"{s:>5}\")\n            })\n            .collect();\n\n            (max_entry_upper, labels)\n        }\n        AxisScaling::Log => {\n            let (m_limit, g_limit, t_limit) = if use_binary_prefix {\n                (LOG_MEBI_LIMIT, LOG_GIBI_LIMIT, LOG_TEBI_LIMIT)\n            } else {\n                (LOG_MEGA_LIMIT, LOG_GIGA_LIMIT, LOG_TERA_LIMIT)\n            };\n\n            // Remember to do saturating log checks as otherwise 0.0 becomes inf, and you get\n            // gaps!\n            let max_entry = if use_binary_prefix {\n                saturating_log2(max_entry)\n            } else {\n                saturating_log10(max_entry)\n            };\n\n            fn get_zero(network_use_binary_prefix: bool, unit_char: &str) -> String {\n                format!(\n                    \"{}0{}\",\n                    if network_use_binary_prefix { \"  \" } else { \" \" },\n                    unit_char\n                )\n            }\n\n            fn get_k(network_use_binary_prefix: bool, unit_char: &str) -> String {\n                format!(\n                    \"1{}{}\",\n                    if network_use_binary_prefix { \"Ki\" } else { \"K\" },\n                    unit_char\n                )\n            }\n\n            fn get_m(network_use_binary_prefix: bool, unit_char: &str) -> String {\n                format!(\n                    \"1{}{}\",\n                    if network_use_binary_prefix { \"Mi\" } else { \"M\" },\n                    unit_char\n                )\n            }\n\n            fn get_g(network_use_binary_prefix: bool, unit_char: &str) -> String {\n                format!(\n                    \"1{}{}\",\n                    if network_use_binary_prefix { \"Gi\" } else { \"G\" },\n                    unit_char\n                )\n            }\n\n            fn get_t(network_use_binary_prefix: bool, unit_char: &str) -> String {\n                format!(\n                    \"1{}{}\",\n                    if network_use_binary_prefix { \"Ti\" } else { \"T\" },\n                    unit_char\n                )\n            }\n\n            fn get_p(network_use_binary_prefix: bool, unit_char: &str) -> String {\n                format!(\n                    \"1{}{}\",\n                    if network_use_binary_prefix { \"Pi\" } else { \"P\" },\n                    unit_char\n                )\n            }\n\n            if max_entry < m_limit {\n                (\n                    m_limit,\n                    vec![\n                        get_zero(use_binary_prefix, unit_char),\n                        get_k(use_binary_prefix, unit_char),\n                        get_m(use_binary_prefix, unit_char),\n                    ],\n                )\n            } else if max_entry < g_limit {\n                (\n                    g_limit,\n                    vec![\n                        get_zero(use_binary_prefix, unit_char),\n                        get_k(use_binary_prefix, unit_char),\n                        get_m(use_binary_prefix, unit_char),\n                        get_g(use_binary_prefix, unit_char),\n                    ],\n                )\n            } else if max_entry < t_limit {\n                (\n                    t_limit,\n                    vec![\n                        get_zero(use_binary_prefix, unit_char),\n                        get_k(use_binary_prefix, unit_char),\n                        get_m(use_binary_prefix, unit_char),\n                        get_g(use_binary_prefix, unit_char),\n                        get_t(use_binary_prefix, unit_char),\n                    ],\n                )\n            } else {\n                // I really doubt anyone's transferring beyond petabyte speeds...\n                (\n                    if use_binary_prefix {\n                        LOG_PEBI_LIMIT\n                    } else {\n                        LOG_PETA_LIMIT\n                    },\n                    vec![\n                        get_zero(use_binary_prefix, unit_char),\n                        get_k(use_binary_prefix, unit_char),\n                        get_m(use_binary_prefix, unit_char),\n                        get_g(use_binary_prefix, unit_char),\n                        get_t(use_binary_prefix, unit_char),\n                        get_p(use_binary_prefix, unit_char),\n                    ],\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/process_table.rs",
    "content": "use tui::{\n    Frame,\n    layout::{Alignment, Constraint, Direction, Layout, Rect},\n    style::Style,\n    text::{Line, Span},\n    widgets::Paragraph,\n};\nuse unicode_segmentation::UnicodeSegmentation;\n\nuse crate::{\n    app::{App, AppSearchState},\n    canvas::{\n        Painter,\n        components::data_table::{DrawInfo, SelectionState},\n        drawing_utils::widget_block,\n    },\n};\n\nconst SORT_MENU_WIDTH: u16 = 7;\n\nimpl Painter {\n    /// Draws and handles all process-related drawing.  Use this.\n    /// - `widget_id` here represents the widget ID of the process widget\n    ///   itself!\n    pub fn draw_process(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        if let Some(proc_widget_state) = app_state.states.proc_state.widget_states.get(&widget_id) {\n            let is_basic = app_state.app_config_fields.use_basic_mode;\n            let search_height = if !is_basic { 5 } else { 3 };\n            let is_sort_open = proc_widget_state.is_sort_open;\n\n            let mut proc_draw_loc = draw_loc;\n            if proc_widget_state.is_search_enabled() {\n                let processes_chunk = Layout::default()\n                    .direction(Direction::Vertical)\n                    .constraints([Constraint::Min(0), Constraint::Length(search_height)])\n                    .split(draw_loc);\n                proc_draw_loc = processes_chunk[0];\n\n                self.draw_search_field(f, app_state, processes_chunk[1], widget_id + 1);\n            }\n\n            if is_sort_open {\n                let processes_chunk = Layout::default()\n                    .direction(Direction::Horizontal)\n                    .constraints([Constraint::Length(SORT_MENU_WIDTH + 4), Constraint::Min(0)])\n                    .split(proc_draw_loc);\n                proc_draw_loc = processes_chunk[1];\n\n                self.draw_sort_table(f, app_state, processes_chunk[0], widget_id + 2);\n            }\n\n            self.draw_processes_table(f, app_state, proc_draw_loc, widget_id);\n        }\n\n        if let Some(proc_widget_state) = app_state\n            .states\n            .proc_state\n            .widget_states\n            .get_mut(&widget_id)\n        {\n            // Reset redraw marker.\n            if proc_widget_state.force_rerender {\n                proc_widget_state.force_rerender = false;\n            }\n        }\n    }\n\n    /// Draws the process sort box.\n    /// - `widget_id` represents the widget ID of the process widget itself.\n    fn draw_processes_table(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        let should_get_widget_bounds = app_state.should_get_widget_bounds();\n        if let Some(proc_widget_state) = app_state\n            .states\n            .proc_state\n            .widget_states\n            .get_mut(&widget_id)\n        {\n            let recalculate_column_widths =\n                should_get_widget_bounds || proc_widget_state.force_rerender;\n\n            let is_on_widget = widget_id == app_state.current_widget.widget_id;\n\n            let draw_info = DrawInfo {\n                loc: draw_loc,\n                force_redraw: app_state.is_force_redraw,\n                recalculate_column_widths,\n                selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),\n            };\n\n            proc_widget_state.table.draw(\n                f,\n                &draw_info,\n                app_state.widget_map.get_mut(&widget_id),\n                self,\n            );\n        }\n    }\n\n    /// Draws the process search field.\n    /// - `widget_id` represents the widget ID of the search box itself --- NOT\n    ///   the process widget state that is stored.\n    fn draw_search_field(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        fn build_query_span(\n            search_state: &AppSearchState, available_width: usize, is_on_widget: bool,\n            currently_selected_text_style: Style, text_style: Style,\n        ) -> Vec<Span<'_>> {\n            let start_index = search_state.display_start_char_index;\n            let cursor_index = search_state.grapheme_cursor.cur_cursor();\n            let mut current_width = 0;\n            let query = search_state.current_search_query.as_str();\n\n            if is_on_widget {\n                let mut res = Vec::with_capacity(available_width);\n                for ((index, grapheme), lengths) in\n                    UnicodeSegmentation::grapheme_indices(query, true)\n                        .zip(search_state.size_mappings.values())\n                {\n                    if index < start_index {\n                        continue;\n                    } else if current_width > available_width {\n                        break;\n                    } else {\n                        let styled = if index == cursor_index {\n                            Span::styled(grapheme, currently_selected_text_style)\n                        } else {\n                            Span::styled(grapheme, text_style)\n                        };\n\n                        res.push(styled);\n                        current_width += lengths.end - lengths.start;\n                    }\n                }\n\n                if cursor_index == query.len() {\n                    res.push(Span::styled(\" \", currently_selected_text_style))\n                }\n\n                res\n            } else {\n                // This is easier - we just need to get a range of graphemes, rather than\n                // dealing with possibly inserting a cursor (as none is shown!)\n\n                vec![Span::styled(query.to_string(), text_style)]\n            }\n        }\n\n        let is_basic = app_state.app_config_fields.use_basic_mode;\n\n        if let Some(proc_widget_state) = app_state\n            .states\n            .proc_state\n            .widget_states\n            .get_mut(&(widget_id - 1))\n        {\n            let is_selected = widget_id == app_state.current_widget.widget_id;\n            let num_columns = usize::from(draw_loc.width);\n            const SEARCH_TITLE: &str = \"> \";\n            let offset = 4;\n            let available_width = if num_columns > (offset + 3) {\n                num_columns - offset\n            } else {\n                num_columns\n            };\n\n            proc_widget_state\n                .proc_search\n                .search_state\n                .get_start_position(available_width, app_state.is_force_redraw);\n\n            // TODO: [CURSOR] blinking cursor?\n            let query_with_cursor = build_query_span(\n                &proc_widget_state.proc_search.search_state,\n                available_width,\n                is_selected,\n                self.styles.selected_text_style,\n                self.styles.text_style,\n            );\n\n            let mut search_text = vec![Line::from({\n                let mut search_vec = vec![Span::styled(\n                    SEARCH_TITLE,\n                    if is_selected {\n                        self.styles.table_header_style\n                    } else {\n                        self.styles.text_style\n                    },\n                )];\n                search_vec.extend(query_with_cursor);\n\n                search_vec\n            })];\n\n            // Text options shamelessly stolen from VS Code.\n            let case_style = if !proc_widget_state.proc_search.query_options.ignore_case {\n                self.styles.selected_text_style\n            } else {\n                self.styles.text_style\n            };\n\n            let whole_word_style = if proc_widget_state.proc_search.query_options.whole_word {\n                self.styles.selected_text_style\n            } else {\n                self.styles.text_style\n            };\n\n            let regex_style = if proc_widget_state.proc_search.query_options.use_regex {\n                self.styles.selected_text_style\n            } else {\n                self.styles.text_style\n            };\n\n            // TODO: [MOUSE] Mouse support for these in search\n            // TODO: [MOVEMENT] Movement support for these in search\n            let (case, whole, regex) = {\n                cfg_if::cfg_if! {\n                    if #[cfg(target_os = \"macos\")] {\n                        (\"Case(F1)\", \"Whole(F2)\", \"Regex(F3)\")\n                    } else {\n                        (\"Case(Alt+C)\", \"Whole(Alt+W)\", \"Regex(Alt+R)\")\n                    }\n                }\n            };\n            let option_text = Line::from(vec![\n                Span::styled(case, case_style),\n                Span::raw(\"  \"),\n                Span::styled(whole, whole_word_style),\n                Span::raw(\"  \"),\n                Span::styled(regex, regex_style),\n            ]);\n\n            search_text.push(Line::from(Span::styled(\n                if let Some(err) = &proc_widget_state.proc_search.search_state.error_message {\n                    err.as_str()\n                } else {\n                    \"\"\n                },\n                self.styles.invalid_query_style,\n            )));\n            search_text.push(option_text);\n\n            let current_border_style =\n                if proc_widget_state.proc_search.search_state.is_invalid_search {\n                    self.styles.invalid_query_style\n                } else if is_selected {\n                    self.styles.highlighted_border_style\n                } else {\n                    self.styles.border_style\n                };\n\n            let process_search_block = {\n                let mut block = widget_block(is_basic, is_selected, self.styles.border_type)\n                    .border_style(current_border_style);\n\n                if !is_basic {\n                    block = block.title_top(\n                        Line::styled(\" Esc to close \", current_border_style).right_aligned(),\n                    )\n                }\n\n                block\n            };\n\n            let margined_draw_loc = Layout::default()\n                .constraints([Constraint::Percentage(100)])\n                .horizontal_margin(u16::from(is_basic && !is_selected))\n                .direction(Direction::Horizontal)\n                .split(draw_loc)[0];\n\n            f.render_widget(\n                Paragraph::new(search_text)\n                    .block(process_search_block)\n                    .style(self.styles.text_style)\n                    .alignment(Alignment::Left),\n                margined_draw_loc,\n            );\n\n            if app_state.should_get_widget_bounds() {\n                // Update draw loc in widget map\n                if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {\n                    widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));\n                    widget.bottom_right_corner = Some((\n                        margined_draw_loc.x + margined_draw_loc.width,\n                        margined_draw_loc.y + margined_draw_loc.height,\n                    ));\n                }\n            }\n        }\n    }\n\n    /// Draws the process sort box.\n    /// - `widget_id` represents the widget ID of the sort box itself --- NOT\n    ///   the process widget state that is stored.\n    fn draw_sort_table(\n        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,\n    ) {\n        let should_get_widget_bounds = app_state.should_get_widget_bounds();\n        if let Some(pws) = app_state\n            .states\n            .proc_state\n            .widget_states\n            .get_mut(&(widget_id - 2))\n        {\n            let recalculate_column_widths = should_get_widget_bounds || pws.force_rerender;\n\n            let is_on_widget = widget_id == app_state.current_widget.widget_id;\n\n            let draw_info = DrawInfo {\n                loc: draw_loc,\n                force_redraw: app_state.is_force_redraw,\n                recalculate_column_widths,\n                selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),\n            };\n\n            pws.sort_table.draw(\n                f,\n                &draw_info,\n                app_state.widget_map.get_mut(&widget_id),\n                self,\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas/widgets/temperature_table.rs",
    "content": "use tui::{Frame, layout::Rect};\n\nuse crate::{\n    app,\n    canvas::{\n        Painter,\n        components::data_table::{DrawInfo, SelectionState},\n    },\n};\n\nimpl Painter {\n    pub fn draw_temp_table(\n        &self, f: &mut Frame<'_>, app_state: &mut app::App, draw_loc: Rect, widget_id: u64,\n    ) {\n        let recalculate_column_widths = app_state.should_get_widget_bounds();\n        if let Some(temp_widget_state) = app_state\n            .states\n            .temp_state\n            .widget_states\n            .get_mut(&widget_id)\n        {\n            let is_on_widget = app_state.current_widget.widget_id == widget_id;\n\n            let draw_info = DrawInfo {\n                loc: draw_loc,\n                force_redraw: app_state.is_force_redraw,\n                recalculate_column_widths,\n                selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),\n            };\n\n            temp_widget_state.table.draw(\n                f,\n                &draw_info,\n                app_state.widget_map.get_mut(&widget_id),\n                self,\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/canvas.rs",
    "content": "//! Code related to drawing.\n//!\n//! Note that eventually this should not contain any widget-specific draw code, but rather just generic code\n//! or components.\n\npub mod components;\npub mod dialogs;\nmod drawing_utils;\nmod widgets;\n\nuse tui::{\n    Frame, Terminal,\n    backend::Backend,\n    layout::{Constraint, Direction, Flex, Layout, Rect},\n    text::Span,\n    widgets::Paragraph,\n};\n\nuse crate::{\n    app::{\n        App,\n        layout_manager::{BottomColRow, BottomLayout, BottomWidgetType},\n    },\n    constants::*,\n    options::config::style::Styles,\n};\n\n/// Handles the canvas' state.\npub struct Painter {\n    pub styles: Styles,\n\n    /// Used to know whether to invalidate things.\n    previous_height: u16,\n\n    /// Used to know whether to invalidate things.\n    previous_width: u16,\n\n    /// The layout.\n    layout: BottomLayout,\n}\n\nimpl Painter {\n    pub fn init(layout: BottomLayout, styling: Styles) -> anyhow::Result<Self> {\n        let painter = Painter {\n            styles: styling,\n            previous_height: 0,\n            previous_width: 0,\n            layout,\n        };\n\n        Ok(painter)\n    }\n\n    /// Determines the border style.\n    pub fn get_border_style(&self, widget_id: u64, selected_widget_id: u64) -> tui::style::Style {\n        let is_on_widget = widget_id == selected_widget_id;\n        if is_on_widget {\n            self.styles.highlighted_border_style\n        } else {\n            self.styles.border_style\n        }\n    }\n\n    fn draw_frozen_indicator(&self, f: &mut Frame<'_>, draw_loc: Rect) {\n        f.render_widget(\n            Paragraph::new(Span::styled(\n                \"Frozen, press 'f' to unfreeze\",\n                self.styles.selected_text_style,\n            )),\n            Layout::default()\n                .horizontal_margin(1)\n                .constraints([Constraint::Length(1)])\n                .split(draw_loc)[0],\n        )\n    }\n\n    pub fn draw_data<B: Backend>(\n        &mut self, terminal: &mut Terminal<B>, app_state: &mut App,\n    ) -> Result<(), B::Error> {\n        use BottomWidgetType::*;\n\n        terminal.draw(|f| {\n            let (terminal_size, frozen_draw_loc) = if app_state.data_store.is_frozen() {\n                // TODO: Remove built-in cache?\n                let split_loc = Layout::default()\n                    .constraints([Constraint::Min(0), Constraint::Length(1)])\n                    .split(f.area());\n                (split_loc[0], Some(split_loc[1]))\n            } else {\n                (f.area(), None)\n            };\n            let terminal_height = terminal_size.height;\n            let terminal_width = terminal_size.width;\n\n            if (self.previous_height == 0 && self.previous_width == 0)\n                || (self.previous_height != terminal_height\n                    || self.previous_width != terminal_width)\n            {\n                app_state.is_force_redraw = true;\n                self.previous_height = terminal_height;\n                self.previous_width = terminal_width;\n            }\n\n            // TODO: We should probably remove this or make it done elsewhere, not the responsibility of the app.\n            if app_state.should_get_widget_bounds() {\n                // If we're force drawing, reset ALL mouse boundaries.\n                for widget in app_state.widget_map.values_mut() {\n                    widget.top_left_corner = None;\n                    widget.bottom_right_corner = None;\n                }\n\n                // Reset process kill dialog button locations...\n                app_state.process_kill_dialog.handle_redraw();\n\n                // Reset battery dialog button locations...\n                for battery_widget in app_state.states.battery_state.widget_states.values_mut() {\n                    battery_widget.tab_click_locs = None;\n                }\n            }\n\n            // TODO: Make drawing dialog generic.\n            if app_state.help_dialog_state.is_showing_help {\n                let gen_help_len = GENERAL_HELP_TEXT.len() as u16 + 3;\n                let border_len = terminal_height.saturating_sub(gen_help_len) / 2;\n                let [_, vertical_dialog_chunk, _] = Layout::default()\n                    .direction(Direction::Vertical)\n                    .constraints([\n                        Constraint::Length(border_len),\n                        Constraint::Length(gen_help_len),\n                        Constraint::Length(border_len),\n                    ])\n                    .areas(terminal_size);\n\n                // An approximate proxy for the max line length to use.\n                const MAX_TEXT_LENGTH: u16 = const {\n                    let mut max = 0;\n\n                    let mut i = 0;\n                    while i < HELP_TEXT.len() {\n                        let section = HELP_TEXT[i];\n                        let mut j = 0;\n                        while j < section.len() {\n                            let line = section[j];\n                            if line.len() > max {\n                                max = line.len();\n                            }\n\n                            j += 1;\n                        }\n\n                        i += 1;\n                    }\n\n                    max as u16\n                };\n\n                let dialog_width = vertical_dialog_chunk.width;\n                let [middle_dialog_chunk] = if dialog_width < MAX_TEXT_LENGTH {\n                    Layout::default()\n                        .direction(Direction::Horizontal)\n                        .constraints([Constraint::Percentage(100)])\n                        .areas(vertical_dialog_chunk)\n                } else {\n                    // We calculate this so that the margins never have to split an odd number.\n                    let len = if (dialog_width.saturating_sub(MAX_TEXT_LENGTH)) % 2 == 0 {\n                        MAX_TEXT_LENGTH\n                    } else {\n                        // It can only be 1 if the difference is greater than 1, so this is fine.\n                        MAX_TEXT_LENGTH + 1\n                    };\n\n                    Layout::default()\n                        .direction(Direction::Horizontal)\n                        .constraints([Constraint::Length(len)])\n                        .flex(Flex::SpaceAround)\n                        .areas(vertical_dialog_chunk)\n                };\n\n                self.draw_help_dialog(f, app_state, middle_dialog_chunk);\n            } else if app_state.process_kill_dialog.is_open() {\n                // FIXME: For width, just limit to a max size or full width. For height, not sure. Maybe pass max and let child handle?\n                let horizontal_padding = if terminal_width < 100 { 0 } else { 5 };\n                let vertical_padding = if terminal_height < 100 { 0 } else { 5 };\n\n                let vertical_dialog_chunk = Layout::default()\n                    .direction(Direction::Vertical)\n                    .constraints([\n                        Constraint::Length(vertical_padding),\n                        Constraint::Fill(1),\n                        Constraint::Length(vertical_padding),\n                    ])\n                    .areas::<3>(terminal_size)[1];\n\n                let dialog_draw_area = Layout::default()\n                    .direction(Direction::Horizontal)\n                    .constraints([\n                        Constraint::Length(horizontal_padding),\n                        Constraint::Fill(1),\n                        Constraint::Length(horizontal_padding),\n                    ])\n                    .areas::<3>(vertical_dialog_chunk)[1];\n\n                app_state\n                    .process_kill_dialog\n                    .draw(f, dialog_draw_area, &self.styles);\n            } else if app_state.is_expanded {\n                if let Some(frozen_draw_loc) = frozen_draw_loc {\n                    self.draw_frozen_indicator(f, frozen_draw_loc);\n                }\n\n                let rect = Layout::default()\n                    .margin(0)\n                    .constraints([Constraint::Percentage(100)])\n                    .split(terminal_size);\n                match &app_state.current_widget.widget_type {\n                    Cpu => self.draw_cpu(f, app_state, rect[0], app_state.current_widget.widget_id),\n                    CpuLegend => self.draw_cpu(\n                        f,\n                        app_state,\n                        rect[0],\n                        app_state.current_widget.widget_id - 1,\n                    ),\n                    Mem | BasicMem => self.draw_memory_graph(\n                        f,\n                        app_state,\n                        rect[0],\n                        app_state.current_widget.widget_id,\n                    ),\n                    Disk => self.draw_disk_table(\n                        f,\n                        app_state,\n                        rect[0],\n                        app_state.current_widget.widget_id,\n                    ),\n                    Temp => self.draw_temp_table(\n                        f,\n                        app_state,\n                        rect[0],\n                        app_state.current_widget.widget_id,\n                    ),\n                    Net => {\n                        self.draw_network(f, app_state, rect[0], app_state.current_widget.widget_id)\n                    }\n                    Proc | ProcSearch | ProcSort => {\n                        let widget_id = app_state.current_widget.widget_id\n                            - match &app_state.current_widget.widget_type {\n                                ProcSearch => 1,\n                                ProcSort => 2,\n                                _ => 0,\n                            };\n\n                        self.draw_process(f, app_state, rect[0], widget_id);\n                    }\n                    Battery =>\n                    {\n                        #[cfg(feature = \"battery\")]\n                        self.draw_battery(f, app_state, rect[0], app_state.current_widget.widget_id)\n                    }\n                    _ => {}\n                }\n            } else if app_state.app_config_fields.use_basic_mode {\n                // Basic mode. This basically removes all graphs but otherwise\n                // the same info.\n                if let Some(frozen_draw_loc) = frozen_draw_loc {\n                    self.draw_frozen_indicator(f, frozen_draw_loc);\n                }\n\n                let data = app_state.data_store.get_data();\n                let actual_cpu_data_len = data.cpu_harvest.len();\n\n                // This fixes #397, apparently if the height is 1, it can't render the CPU\n                // bars...\n                let cpu_height = {\n                    let c = (actual_cpu_data_len / 4) as u16\n                        + u16::from(actual_cpu_data_len % 4 != 0)\n                        + u16::from(\n                            app_state.app_config_fields.show_average_cpu\n                                && app_state.app_config_fields.dedicated_average_row\n                                && actual_cpu_data_len.saturating_sub(1) % 4 != 0,\n                        );\n\n                    if c <= 1 { 1 } else { c }\n                };\n\n                let mut mem_rows = 1;\n\n                if data.swap_harvest.is_some() {\n                    mem_rows += 1; // add row for swap\n                }\n\n                #[cfg(feature = \"zfs\")]\n                {\n                    if data.arc_harvest.is_some() {\n                        mem_rows += 1; // add row for arc\n                    }\n                }\n\n                #[cfg(not(target_os = \"windows\"))]\n                {\n                    if data.cache_harvest.is_some() {\n                        mem_rows += 1;\n                    }\n                }\n\n                #[cfg(feature = \"gpu\")]\n                {\n                    mem_rows += data.gpu_harvest.len() as u16; // add row(s) for gpu\n                }\n\n                let network_rows = if app_state.app_config_fields.network_show_packets {\n                    4 // 4 rows for RX/TX and Packet Rates (Avg sizes moved to right side)\n                } else {\n                    2 // 2 rows for RX and TX\n                };\n\n                if mem_rows < network_rows {\n                    mem_rows += network_rows - mem_rows; // min rows\n                }\n\n                let vertical_chunks = Layout::default()\n                    .direction(Direction::Vertical)\n                    .margin(0)\n                    .constraints([\n                        Constraint::Length(cpu_height),\n                        Constraint::Length(mem_rows),\n                        Constraint::Length(2),\n                        Constraint::Min(5),\n                    ])\n                    .split(terminal_size);\n\n                let middle_chunks = Layout::default()\n                    .direction(Direction::Horizontal)\n                    .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])\n                    .split(vertical_chunks[1]);\n\n                if vertical_chunks[0].width >= 2 {\n                    self.draw_basic_cpu(f, app_state, vertical_chunks[0], 1);\n                }\n                if middle_chunks[0].width >= 2 {\n                    self.draw_basic_memory(f, app_state, middle_chunks[0], 2);\n                }\n                if middle_chunks[1].width >= 2 {\n                    self.draw_basic_network(f, app_state, middle_chunks[1], 3);\n                }\n\n                let mut later_widget_id: Option<u64> = None;\n                if let Some(basic_table_widget_state) = &app_state.states.basic_table_widget_state {\n                    let widget_id = basic_table_widget_state.currently_displayed_widget_id;\n                    later_widget_id = Some(widget_id);\n                    if vertical_chunks[3].width >= 2 {\n                        match basic_table_widget_state.currently_displayed_widget_type {\n                            Disk => {\n                                self.draw_disk_table(f, app_state, vertical_chunks[3], widget_id)\n                            }\n                            Proc | ProcSort => {\n                                let wid = widget_id\n                                    - match basic_table_widget_state.currently_displayed_widget_type\n                                    {\n                                        ProcSearch => 1,\n                                        ProcSort => 2,\n                                        _ => 0,\n                                    };\n                                self.draw_process(f, app_state, vertical_chunks[3], wid);\n                            }\n                            Temp => {\n                                self.draw_temp_table(f, app_state, vertical_chunks[3], widget_id)\n                            }\n                            Battery =>\n                            {\n                                #[cfg(feature = \"battery\")]\n                                self.draw_battery(f, app_state, vertical_chunks[3], widget_id)\n                            }\n                            _ => {}\n                        }\n                    }\n                }\n\n                if let Some(widget_id) = later_widget_id {\n                    self.draw_basic_table_arrows(f, app_state, vertical_chunks[2], widget_id);\n                }\n            } else {\n                // Draws using the passed in (or default) layout.\n                if let Some(frozen_draw_loc) = frozen_draw_loc {\n                    self.draw_frozen_indicator(f, frozen_draw_loc);\n                }\n\n                // A two-pass algorithm - get layouts using constraints (first pass),\n                // then pass each layout to the corresponding widget (second pass).\n                // Note that layouts are already cached in ratatui, so we don't need\n                // to do it manually!\n                let base = Layout::vertical(self.layout.rows.iter().map(|r| r.constraint))\n                    .split(terminal_size);\n\n                for (br, base) in self.layout.rows.iter().zip(base.iter()) {\n                    let base =\n                        Layout::horizontal(br.children.iter().map(|bc| bc.constraint)).split(*base);\n\n                    for (bc, base) in br.children.iter().zip(base.iter()) {\n                        let base = Layout::vertical(bc.children.iter().map(|bcr| bcr.constraint))\n                            .split(*base);\n\n                        for (widgets, base) in bc.children.iter().zip(base.iter()) {\n                            let widget_draw_locs =\n                                Layout::horizontal(widgets.children.iter().map(|bw| bw.constraint))\n                                    .split(*base);\n\n                            self.draw_widgets_with_constraints(\n                                f,\n                                app_state,\n                                widgets,\n                                &widget_draw_locs,\n                            );\n                        }\n                    }\n                }\n            }\n        })?;\n\n        if let Some(updated_current_widget) = app_state\n            .widget_map\n            .get(&app_state.current_widget.widget_id)\n        {\n            app_state.current_widget = updated_current_widget.clone();\n        }\n\n        app_state.is_force_redraw = false;\n        app_state.is_determining_widget_boundary = false;\n\n        Ok(())\n    }\n\n    fn draw_widgets_with_constraints(\n        &self, f: &mut Frame<'_>, app_state: &mut App, widgets: &BottomColRow,\n        widget_draw_locs: &[Rect],\n    ) {\n        use BottomWidgetType::*;\n        for (widget, draw_loc) in widgets.children.iter().zip(widget_draw_locs) {\n            if draw_loc.width >= 2 && draw_loc.height >= 2 {\n                match &widget.widget_type {\n                    Cpu => self.draw_cpu(f, app_state, *draw_loc, widget.widget_id),\n                    Mem => self.draw_memory_graph(f, app_state, *draw_loc, widget.widget_id),\n                    Net => self.draw_network(f, app_state, *draw_loc, widget.widget_id),\n                    Temp => self.draw_temp_table(f, app_state, *draw_loc, widget.widget_id),\n                    Disk => self.draw_disk_table(f, app_state, *draw_loc, widget.widget_id),\n                    Proc => self.draw_process(f, app_state, *draw_loc, widget.widget_id),\n                    Battery =>\n                    {\n                        #[cfg(feature = \"battery\")]\n                        self.draw_battery(f, app_state, *draw_loc, widget.widget_id)\n                    }\n                    _ => {}\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/collection/amd/amd_gpu_marketing.rs",
    "content": "// from https://github.com/GPUOpen-Tools/device_info/blob/master/DeviceInfo.cpp\n\npub const AMDGPU_DEFAULT_NAME: &str = \"AMD Radeon Graphics\";\n\npub const AMD_GPU_MARKETING_NAME: &[(u32, u32, &str)] = &[\n    (0x6798, 0x00, \"AMD Radeon R9 200 / HD 7900\"),\n    (0x6799, 0x00, \"AMD Radeon HD 7900\"),\n    (0x679A, 0x00, \"AMD Radeon HD 7900\"),\n    (0x679B, 0x00, \"AMD Radeon HD 7900\"),\n    (0x679E, 0x00, \"AMD Radeon HD 7800\"),\n    (0x6780, 0x00, \"AMD FirePro W9000\"),\n    (0x6784, 0x00, \"ATI FirePro V\"),\n    (0x6788, 0x00, \"ATI FirePro V\"),\n    (0x678A, 0x00, \"AMD FirePro W8000\"),\n    (0x6818, 0x00, \"AMD Radeon HD 7800\"),\n    (0x6819, 0x00, \"AMD Radeon HD 7800\"),\n    (0x6808, 0x00, \"AMD FirePro W7000\"),\n    (0x6809, 0x00, \"ATI FirePro W5000\"),\n    (0x684C, 0x00, \"ATI FirePro V\"),\n    (0x6800, 0x00, \"AMD Radeon HD 7970M\"),\n    (0x6801, 0x00, \"AMD Radeon HD8970M\"),\n    (0x6806, 0x00, \"AMD Radeon R9 M290X\"),\n    (0x6810, 0x00, \"AMD Radeon R9 200\"),\n    (0x6810, 0x81, \"AMD Radeon R9 370\"),\n    (0x6811, 0x00, \"AMD Radeon R9 200\"),\n    (0x6811, 0x81, \"AMD Radeon R7 370\"),\n    (0x6820, 0x00, \"AMD Radeon R9 M275X\"),\n    (0x6820, 0x81, \"AMD Radeon R9 M375\"),\n    (0x6820, 0x83, \"AMD Radeon R9 M375X\"),\n    (0x6821, 0x00, \"AMD Radeon R9 M200X\"),\n    (0x6821, 0x83, \"AMD Radeon R9 M370X\"),\n    (0x6821, 0x87, \"AMD Radeon R7 M380\"),\n    (0x6822, 0x00, \"AMD Radeon E8860\"),\n    (0x6823, 0x00, \"AMD Radeon R9 M200X\"),\n    (0x6825, 0x00, \"AMD Radeon HD 7800M\"),\n    (0x6826, 0x00, \"AMD Radeon HD 7700M\"),\n    (0x6827, 0x00, \"AMD Radeon HD 7800M\"),\n    (0x682B, 0x00, \"AMD Radeon HD 8800M\"),\n    (0x682B, 0x87, \"AMD Radeon R9 M360\"),\n    (0x682D, 0x00, \"AMD Radeon HD 7700M\"),\n    (0x682F, 0x00, \"AMD Radeon HD 7700M\"),\n    (0x6828, 0x00, \"AMD FirePro W600\"),\n    (0x682C, 0x00, \"AMD FirePro W4100\"),\n    (0x6830, 0x00, \"AMD Radeon 7800M\"),\n    (0x6831, 0x00, \"AMD Radeon 7700M\"),\n    (0x6835, 0x00, \"AMD Radeon R7 Series / HD 9000\"),\n    (0x6837, 0x00, \"AMD Radeon HD 7700\"),\n    (0x683D, 0x00, \"AMD Radeon HD 7700\"),\n    (0x683F, 0x00, \"AMD Radeon HD 7700\"),\n    (0x6608, 0x00, \"AMD FirePro W2100\"),\n    (0x6610, 0x00, \"AMD Radeon R7 200\"),\n    (0x6610, 0x81, \"AMD Radeon R7 350\"),\n    (0x6610, 0x83, \"AMD Radeon R5 340\"),\n    (0x6610, 0x87, \"AMD Radeon R7 200\"),\n    (0x6611, 0x00, \"AMD Radeon R7 200\"),\n    (0x6611, 0x87, \"AMD Radeon R7 200\"),\n    (0x6613, 0x00, \"AMD Radeon R7 200\"),\n    (0x6617, 0x00, \"AMD Radeon R7 240\"),\n    (0x6617, 0x87, \"AMD Radeon R7 200\"),\n    (0x6617, 0xC7, \"AMD Radeon R7 240\"),\n    (0x6600, 0x00, \"AMD Radeon HD 8600/8700M\"),\n    (0x6600, 0x81, \"AMD Radeon R7 M370\"),\n    (0x6601, 0x00, \"AMD Radeon HD 8500M/8700M\"),\n    (0x6604, 0x00, \"AMD Radeon R7 M265\"),\n    (0x6604, 0x81, \"AMD Radeon R7 M350\"),\n    (0x6605, 0x00, \"AMD Radeon R7 M260\"),\n    (0x6605, 0x81, \"AMD Radeon R7 M340\"),\n    (0x6606, 0x00, \"AMD Radeon HD 8790M\"),\n    (0x6607, 0x00, \"AMD Radeon R5 M240\"),\n    (0x6660, 0x00, \"AMD Radeon HD 8600M\"),\n    (0x6660, 0x81, \"AMD Radeon R5 M335\"),\n    (0x6660, 0x83, \"AMD Radeon R5 M330\"),\n    (0x6663, 0x00, \"AMD Radeon HD 8500M\"),\n    (0x6663, 0x83, \"AMD Radeon R5 M320\"),\n    (0x6664, 0x00, \"AMD Radeon R5 M200\"),\n    (0x6665, 0x00, \"AMD Radeon R5 M230\"),\n    (0x6665, 0x83, \"AMD Radeon R5 M320\"),\n    (0x6665, 0xC3, \"AMD Radeon R5 M435\"),\n    (0x6666, 0x00, \"AMD Radeon R5 M200\"),\n    (0x6667, 0x00, \"AMD Radeon R5 M200\"),\n    (0x666F, 0x00, \"AMD Radeon HD 8500M\"),\n    (0x6649, 0x00, \"AMD FirePro W5100\"),\n    (0x6658, 0x00, \"AMD Radeon R7 200\"),\n    (0x665C, 0x00, \"AMD Radeon HD 7700\"),\n    (0x665D, 0x00, \"AMD Radeon R7 200\"),\n    (0x665F, 0x81, \"AMD Radeon R7 360\"),\n    (0x665F, 0x81, \"AMD Radeon R7 360\"),\n    (0x6640, 0x00, \"AMD Radeon HD 8950\"),\n    (0x6640, 0x80, \"AMD Radeon R9 M380\"),\n    (0x6646, 0x00, \"AMD Radeon R9 M280X\"),\n    (0x6646, 0x80, \"AMD Radeon R9 M385\"),\n    (0x6647, 0x00, \"AMD Radeon R9 M200X\"),\n    (0x6647, 0x80, \"AMD Radeon R9 M380\"),\n    (0x67A0, 0x00, \"AMD FirePro W9100\"),\n    (0x67A1, 0x00, \"AMD FirePro W8100\"),\n    (0x67B0, 0x00, \"AMD Radeon R9 200\"),\n    (0x67B0, 0x80, \"AMD Radeon R9 390\"),\n    (0x67B1, 0x00, \"AMD Radeon R9 200\"),\n    (0x67B1, 0x80, \"AMD Radeon R9 390\"),\n    (0x67B9, 0x00, \"AMD Radeon R9 200\"),\n    (0x1309, 0x00, \"AMD Radeon R7\"),\n    (0x130A, 0x00, \"AMD Radeon R6\"),\n    (0x130C, 0x00, \"AMD Radeon R7\"),\n    (0x130D, 0x00, \"AMD Radeon R6\"),\n    (0x130E, 0x00, \"AMD Radeon R5\"),\n    (0x130F, 0x00, \"AMD Radeon R7\"),\n    (0x130F, 0xD4, \"AMD Radeon R7\"),\n    (0x130F, 0xD5, \"AMD Radeon R7\"),\n    (0x130F, 0xD6, \"AMD Radeon R7\"),\n    (0x130F, 0xD7, \"AMD Radeon R7\"),\n    (0x1313, 0x00, \"AMD Radeon R7\"),\n    (0x1313, 0xD4, \"AMD Radeon R7\"),\n    (0x1313, 0xD5, \"AMD Radeon R7\"),\n    (0x1313, 0xD6, \"AMD Radeon R7\"),\n    (0x1315, 0x00, \"AMD Radeon R5\"),\n    (0x1315, 0xD4, \"AMD Radeon R5\"),\n    (0x1315, 0xD5, \"AMD Radeon R5\"),\n    (0x1315, 0xD6, \"AMD Radeon R5\"),\n    (0x1315, 0xD7, \"AMD Radeon R5\"),\n    (0x1318, 0x00, \"AMD Radeon R5\"),\n    (0x131C, 0x00, \"AMD Radeon R7\"),\n    (0x131D, 0x00, \"AMD Radeon R6\"),\n    (0x130B, 0x00, \"AMD Radeon R4\"),\n    (0x1316, 0x00, \"AMD Radeon R5\"),\n    (0x131B, 0x00, \"AMD Radeon R4\"),\n    (0x9830, 0x00, \"AMD Radeon HD 8400 / R3\"),\n    (0x9831, 0x00, \"AMD Radeon HD 8400E\"),\n    (0x9832, 0x00, \"AMD Radeon HD 8330\"),\n    (0x9833, 0x00, \"AMD Radeon HD 8330E\"),\n    (0x9834, 0x00, \"AMD Radeon HD 8210\"),\n    (0x9835, 0x00, \"AMD Radeon HD 8210E\"),\n    (0x9836, 0x00, \"AMD Radeon HD 8200 / R3\"),\n    (0x9837, 0x00, \"AMD Radeon HD 8280E\"),\n    (0x9838, 0x00, \"AMD Radeon HD 8200 / R3\"),\n    (0x9839, 0x00, \"AMD Radeon HD 8180\"),\n    (0x983D, 0x00, \"AMD Radeon HD 8250\"),\n    (0x9850, 0x00, \"AMD Radeon R3\"),\n    (0x9850, 0x03, \"AMD Radeon R3\"),\n    (0x9850, 0x40, \"AMD Radeon R2\"),\n    (0x9850, 0x45, \"AMD Radeon R3\"),\n    (0x9851, 0x00, \"AMD Radeon R4\"),\n    (0x9851, 0x01, \"AMD Radeon R5E\"),\n    (0x9851, 0x05, \"AMD Radeon R5\"),\n    (0x9851, 0x06, \"AMD Radeon R5E\"),\n    (0x9851, 0x40, \"AMD Radeon R4\"),\n    (0x9851, 0x45, \"AMD Radeon R5\"),\n    (0x9852, 0x00, \"AMD Radeon R2\"),\n    (0x9852, 0x40, \"AMD Radeon E1\"),\n    (0x9853, 0x00, \"AMD Radeon R2\"),\n    (0x9853, 0x01, \"AMD Radeon R4E\"),\n    (0x9853, 0x03, \"AMD Radeon R2\"),\n    (0x9853, 0x05, \"AMD Radeon R1E\"),\n    (0x9853, 0x06, \"AMD Radeon R1E\"),\n    (0x9853, 0x40, \"AMD Radeon R2\"),\n    (0x9853, 0x07, \"AMD Radeon R1E\"),\n    (0x9853, 0x08, \"AMD Radeon R1E\"),\n    (0x9854, 0x00, \"AMD Radeon R3\"),\n    (0x9854, 0x01, \"AMD Radeon R3E\"),\n    (0x9854, 0x02, \"AMD Radeon R3\"),\n    (0x9854, 0x05, \"AMD Radeon R2\"),\n    (0x9854, 0x06, \"AMD Radeon R4\"),\n    (0x9854, 0x07, \"AMD Radeon R3\"),\n    (0x9855, 0x02, \"AMD Radeon R6\"),\n    (0x9855, 0x05, \"AMD Radeon R4\"),\n    (0x9856, 0x07, \"AMD Radeon R1E\"),\n    (0x9856, 0x00, \"AMD Radeon R2\"),\n    (0x9856, 0x01, \"AMD Radeon R2E\"),\n    (0x9856, 0x02, \"AMD Radeon R2\"),\n    (0x9856, 0x05, \"AMD Radeon R1E\"),\n    (0x9856, 0x06, \"AMD Radeon R2\"),\n    (0x9856, 0x07, \"AMD Radeon R1E\"),\n    (0x9856, 0x08, \"AMD Radeon R1E\"),\n    (0x9856, 0x13, \"AMD Radeon R1E\"),\n    (0x6900, 0x00, \"AMD Radeon R7 M260\"),\n    (0x6900, 0x81, \"AMD Radeon R7 M360\"),\n    (0x6900, 0x83, \"AMD Radeon R7 M340\"),\n    (0x6900, 0xC1, \"AMD Radeon R5 M465\"),\n    (0x6900, 0xC3, \"AMD Radeon R5 M445\"),\n    (0x6900, 0xD1, \"AMD Radeon 530\"),\n    (0x6900, 0xD3, \"AMD Radeon 530\"),\n    (0x6901, 0x00, \"AMD Radeon R5 M255\"),\n    (0x6902, 0x00, \"AMD Radeon\"),\n    (0x6907, 0x00, \"AMD Radeon R5 M255\"),\n    (0x6907, 0x87, \"AMD Radeon R5 M315\"),\n    (0x6920, 0x00, \"AMD Radeon R9 M395X\"),\n    (0x6920, 0x01, \"AMD Radeon R9 M390X\"),\n    (0x6921, 0x00, \"AMD Radeon R9 M390X\"),\n    (0x6929, 0x00, \"AMD FirePro S7150\"),\n    (0x6929, 0x01, \"AMD FirePro S7100X\"),\n    (0x692B, 0x00, \"AMD FirePro W7100\"),\n    (0x692F, 0x00, \"AMD MxGPU\"),\n    (0x692F, 0x01, \"AMD MxGPU\"),\n    (0x6930, 0xF0, \"AMD MxGPU\"),\n    (0x6938, 0x00, \"AMD Radeon R9 200\"),\n    (0x6938, 0xF1, \"AMD Radeon R9 380\"),\n    (0x6938, 0xF0, \"AMD Radeon R9 200\"),\n    (0x6939, 0x00, \"AMD Radeon R9 200\"),\n    (0x6939, 0xF0, \"AMD Radeon R9 200\"),\n    (0x6939, 0xF1, \"AMD Radeon R9 380\"),\n    (0x9874, 0xC4, \"AMD Radeon R7\"),\n    (0x9874, 0xC5, \"AMD Radeon R6\"),\n    (0x9874, 0xC6, \"AMD Radeon R6\"),\n    (0x9874, 0xC7, \"AMD Radeon R5\"),\n    (0x9874, 0x81, \"AMD Radeon R6\"),\n    (0x9874, 0x84, \"AMD Radeon R7\"),\n    (0x9874, 0x85, \"AMD Radeon R6\"),\n    (0x9874, 0x87, \"AMD Radeon R5\"),\n    (0x9874, 0x88, \"AMD Radeon R7E\"),\n    (0x9874, 0x89, \"AMD Radeon R6E\"),\n    (0x9874, 0xC8, \"AMD Radeon R7\"),\n    (0x9874, 0xC9, \"AMD Radeon R7\"),\n    (0x9874, 0xCA, \"AMD Radeon R5\"),\n    (0x9874, 0xCB, \"AMD Radeon R5\"),\n    (0x9874, 0xCC, \"AMD Radeon R7\"),\n    (0x9874, 0xCD, \"AMD Radeon R7\"),\n    (0x9874, 0xCE, \"AMD Radeon R5\"),\n    (0x9874, 0xE1, \"AMD Radeon R7\"),\n    (0x9874, 0xE2, \"AMD Radeon R7\"),\n    (0x9874, 0xE3, \"AMD Radeon R7\"),\n    (0x9874, 0xE4, \"AMD Radeon R7\"),\n    (0x9874, 0xE5, \"AMD Radeon R5\"),\n    (0x9874, 0xE6, \"AMD Radeon R5\"),\n    (0x7300, 0xC1, \"AMD FirePro S9300 x2\"),\n    (0x7300, 0xC8, \"AMD Radeon R9 Fury\"),\n    (0x7300, 0xC9, \"AMD Radeon Pro Duo\"),\n    (0x7300, 0xCA, \"AMD Radeon R9 Fury\"),\n    (0x7300, 0xCB, \"AMD Radeon R9 Fury\"),\n    (0x730F, 0xC9, \"AMD MxGPU\"),\n    (0x98E4, 0x80, \"AMD Radeon R5E\"),\n    (0x98E4, 0x81, \"AMD Radeon R4E\"),\n    (0x98E4, 0x83, \"AMD Radeon R2E\"),\n    (0x98E4, 0x84, \"AMD Radeon R2E\"),\n    (0x98E4, 0x86, \"AMD Radeon R1E\"),\n    (0x98E4, 0xC0, \"AMD Radeon R4\"),\n    (0x98E4, 0xC1, \"AMD Radeon R5\"),\n    (0x98E4, 0xC2, \"AMD Radeon R4\"),\n    (0x98E4, 0xC4, \"AMD Radeon R5\"),\n    (0x98E4, 0xC6, \"AMD Radeon R5\"),\n    (0x98E4, 0xC8, \"AMD Radeon R4\"),\n    (0x98E4, 0xC9, \"AMD Radeon R4\"),\n    (0x98E4, 0xCA, \"AMD Radeon R5\"),\n    (0x98E4, 0xD0, \"AMD Radeon R2\"),\n    (0x98E4, 0xD1, \"AMD Radeon R2\"),\n    (0x98E4, 0xD2, \"AMD Radeon R2\"),\n    (0x98E4, 0xD4, \"AMD Radeon R2\"),\n    (0x98E4, 0xD9, \"AMD Radeon R5\"),\n    (0x98E4, 0xDA, \"AMD Radeon R5\"),\n    (0x98E4, 0xDB, \"AMD Radeon R3\"),\n    (0x98E4, 0xE1, \"AMD Radeon R3\"),\n    (0x98E4, 0xE2, \"AMD Radeon R3\"),\n    (0x98E4, 0xE9, \"AMD Radeon R4\"),\n    (0x98E4, 0xEA, \"AMD Radeon R4\"),\n    (0x98E4, 0xEB, \"AMD Radeon R4\"),\n    (0x98E4, 0xEB, \"AMD Radeon R3\"),\n    (0x67C0, 0x00, \"AMD Radeon Pro WX 7100\"),\n    (0x67C0, 0x80, \"AMD Radeon E9550\"),\n    (0x67C2, 0x01, \"AMD Radeon Pro V7350x2\"),\n    (0x67C2, 0x02, \"AMD Radeon Pro V7300X\"),\n    (0x67C4, 0x00, \"AMD Radeon Pro WX 7100\"),\n    (0x67C4, 0x80, \"AMD Radeon Embedded E9560\"),\n    (0x67C7, 0x00, \"AMD Radeon Pro WX 5100\"),\n    (0x67C7, 0x80, \"AMD Radeon Embedded E9390\"),\n    (0x67D0, 0x01, \"AMD Radeon Pro V7350x2\"),\n    (0x67FF, 0xE3, \"AMD Radeon E9550\"),\n    (0x67FF, 0xF3, \"AMD Radeon Pro E9565\"),\n    (0x67FF, 0xF7, \"AMD Radeon Pro WX 5100\"),\n    (0x67D0, 0x02, \"AMD Radeon Pro V7300X\"),\n    (0x67DF, 0xC4, \"AMD Radeon RX 480\"),\n    (0x67DF, 0xC5, \"AMD Radeon RX 470\"),\n    (0x67DF, 0xC7, \"AMD Radeon RX 480\"),\n    (0x67DF, 0xCF, \"AMD Radeon RX 470\"),\n    (0x67DF, 0xFF, \"AMD Radeon RX 470\"),\n    (0x67FF, 0xE7, \"AMD Radeon Embedded E9390\"),\n    (0x67DF, 0xC0, \"AMD Radeon Pro 580X\"),\n    (0x67DF, 0xC1, \"AMD Radeon RX 580\"),\n    (0x67DF, 0xC2, \"AMD Radeon RX 570\"),\n    (0x67DF, 0xC3, \"AMD Radeon RX 580\"),\n    (0x67DF, 0xC6, \"AMD Radeon RX 570\"),\n    (0x67DF, 0xC7, \"AMD Radeon RX 480\"),\n    (0x67DF, 0xCF, \"AMD Radeon RX 470\"),\n    (0x67DF, 0xD7, \"AMD Radeon RX 470\"),\n    (0x67DF, 0xE0, \"AMD Radeon RX 470\"),\n    (0x67DF, 0xE1, \"AMD Radeon RX 590\"),\n    (0x67DF, 0xE3, \"AMD Radeon RX\"),\n    (0x67DF, 0xE7, \"AMD Radeon RX 580\"),\n    (0x67DF, 0xEB, \"AMD Radeon Pro 580X\"),\n    (0x67DF, 0xEF, \"AMD Radeon RX 570\"),\n    (0x67DF, 0xF7, \"AMD P30PH\"),\n    (0x67DF, 0xFF, \"AMD Radeon RX 470\"),\n    (0x6FDF, 0xEF, \"AMD Radeon RX 580 2048SP\"),\n    (0x67E0, 0x00, \"AMD Radeon Pro WX\"),\n    (0x67E3, 0x00, \"AMD Radeon Pro WX 4100\"),\n    (0x67E8, 0x00, \"AMD Radeon Pro WX\"),\n    (0x67E8, 0x01, \"AMD Radeon Pro WX\"),\n    (0x67E8, 0x80, \"AMD Radeon E9260\"),\n    (0x67EB, 0x00, \"AMD Radeon Pro V5300X\"),\n    (0x67EF, 0xC0, \"AMD Radeon RX 560\"),\n    (0x67EF, 0xC1, \"AMD Radeon RX 560\"),\n    (0x67EF, 0xC5, \"AMD Radeon RX 560\"),\n    (0x67EF, 0xC7, \"AMD Radeon 550\"),\n    (0x67EF, 0xCF, \"AMD Radeon RX 460\"),\n    (0x67EF, 0xEF, \"AMD Radeon 550\"),\n    (0x67FF, 0xC0, \"AMD Radeon Pro 465\"),\n    (0x67FF, 0xC1, \"AMD Radeon RX 560\"),\n    (0x67EF, 0xC2, \"AMD Radeon Pro\"),\n    (0x67EF, 0xE3, \"AMD Radeon Pro\"),\n    (0x67EF, 0xE5, \"AMD Radeon RX 560\"),\n    (0x67EF, 0xE7, \"AMD Radeon RX 560\"),\n    (0x67EF, 0xE0, \"AMD Radeon RX 560\"),\n    (0x67EF, 0xFF, \"AMD Radeon RX 460\"),\n    (0x67FF, 0xCF, \"AMD Radeon RX 560\"),\n    (0x67FF, 0xEF, \"AMD Radeon RX 560\"),\n    (0x67FF, 0xFF, \"AMD Radeon RX550/550\"),\n    (0x6980, 0x00, \"AMD Radeon Pro WX 3100\"),\n    (0x6981, 0x00, \"AMD Radeon Pro WX 3200\"),\n    (0x6981, 0x01, \"AMD Radeon Pro WX 3200\"),\n    (0x6981, 0x10, \"AMD Radeon Pro WX 3200\"),\n    (0x6985, 0x00, \"AMD Radeon Pro WX 3100\"),\n    (0x6986, 0x00, \"AMD Radeon Pro WX 2100\"),\n    (0x6987, 0x80, \"AMD Embedded Radeon E9171\"),\n    (0x6987, 0xC0, \"AMD Radeon 550X\"),\n    (0x6987, 0xC1, \"AMD Radeon RX 640\"),\n    (0x6987, 0xC3, \"AMD Radeon 540X\"),\n    (0x6987, 0xC7, \"AMD Radeon 540\"),\n    (0x6995, 0x00, \"AMD Radeon Pro WX 2100\"),\n    (0x6997, 0x00, \"AMD Radeon Pro WX 2100\"),\n    (0x699F, 0x81, \"AMD Embedded Radeon E9170\"),\n    (0x699F, 0xC0, \"AMD Radeon 500\"),\n    (0x699F, 0xC1, \"AMD Radeon 540\"),\n    (0x699F, 0xC3, \"AMD Radeon 500\"),\n    (0x699F, 0xC7, \"AMD Radeon RX550/550\"),\n    (0x699F, 0xC9, \"AMD Radeon 540\"),\n    (0x694C, 0xC0, \"AMD Radeon RX Vega M GH\"),\n    (0x694E, 0xC0, \"AMD Radeon RX Vega M GL\"),\n    (0x6860, 0x00, \"AMD Radeon Instinct MI25\"),\n    (0x6860, 0x01, \"AMD Radeon Instinct MI25\"),\n    (0x6860, 0x02, \"AMD Radeon Instinct MI25\"),\n    (0x6860, 0x03, \"AMD Radeon Pro V340\"),\n    (0x6860, 0x04, \"AMD Radeon Instinct MI25x2\"),\n    (0x6860, 0x06, \"AMD Radeon Instinct MI25\"),\n    (0x6860, 0x07, \"AMD Radeon Pro V320\"),\n    (0x6861, 0x00, \"AMD Radeon Pro WX 9100\"),\n    (0x6862, 0x00, \"AMD Radeon Pro SSG\"),\n    (0x6863, 0x00, \"AMD Radeon Vega Frontier Edition\"),\n    (0x6864, 0x03, \"AMD Radeon Pro V340\"),\n    (0x6864, 0x04, \"AMD Instinct MI25x2\"),\n    (0x6864, 0x05, \"AMD Radeon Pro V340\"),\n    (0x6867, 0x00, \"AMD Radeon Pro Vega 56\"),\n    (0x6868, 0x00, \"AMD Radeon Pro WX 8200\"),\n    (0x686C, 0x00, \"AMD Radeon Instinct MI25 MxGPU\"),\n    (0x686C, 0x01, \"AMD Radeon Instinct MI25 MxGPU\"),\n    (0x686C, 0x02, \"AMD Radeon Instinct MI25 MxGPU\"),\n    (0x686C, 0x03, \"AMD Radeon Pro V340 MxGPU\"),\n    (0x686C, 0x04, \"AMD Radeon Instinct MI25x2 MxGPU\"),\n    (0x686C, 0x05, \"AMD Radeon Pro V340 MxGPU\"),\n    (0x686C, 0x06, \"AMD Radeon Instinct MI25 MxGPU\"),\n    (0x687F, 0x01, \"AMD Radeon RX Vega\"),\n    (0x687F, 0xC0, \"AMD Radeon RX Vega\"),\n    (0x687F, 0xC1, \"AMD Radeon RX Vega\"),\n    (0x687F, 0xC3, \"AMD Radeon RX Vega\"),\n    (0x687F, 0xC7, \"AMD Radeon RX Vega\"),\n    (0x15DD, 0x00, \"AMD 15DD\"),\n    (0x15DD, 0x81, \"AMD Radeon Vega 11\"),\n    (0x15DD, 0x82, \"AMD Radeon Vega 8\"),\n    (0x15DD, 0x83, \"AMD Radeon Vega 8\"),\n    (0x15DD, 0x84, \"AMD Radeon Vega 6\"),\n    (0x15DD, 0x85, \"AMD Radeon Vega 3\"),\n    (0x15DD, 0x86, \"AMD Radeon Vega 11\"),\n    (0x15DD, 0x87, \"AMD 15DD\"),\n    (0x15DD, 0x88, \"AMD Radeon Vega 8\"),\n    (0x15DD, 0xC1, \"AMD Radeon RX Vega 11\"),\n    (0x15DD, 0xC2, \"AMD Radeon Vega 8\"),\n    (0x15DD, 0xC3, \"AMD Radeon RX Vega 10\"),\n    (0x15DD, 0xC4, \"AMD Radeon Vega 8\"),\n    (0x15DD, 0xC5, \"AMD Radeon Vega 3\"),\n    (0x15DD, 0xC6, \"AMD Radeon RX Vega 11\"),\n    (0x15DD, 0xC7, \"AMD 15DD\"),\n    (0x15DD, 0xC8, \"AMD Radeon Vega 8\"),\n    (0x15DD, 0xC9, \"AMD Radeon RX Vega 11\"),\n    (0x15DD, 0xCA, \"AMD Radeon Vega 8\"),\n    (0x15DD, 0xCB, \"AMD Radeon Vega 3\"),\n    (0x15DD, 0xCC, \"AMD Radeon Vega 6\"),\n    (0x15DD, 0xCD, \"AMD 15DD\"),\n    (0x15DD, 0xCE, \"AMD Radeon Vega 3\"),\n    (0x15DD, 0xCF, \"AMD Radeon Vega 3\"),\n    (0x15DD, 0xD0, \"AMD Radeon Vega 10\"),\n    (0x15DD, 0xD1, \"AMD Radeon Vega 8\"),\n    (0x15DD, 0xD2, \"AMD 15DD\"),\n    (0x15DD, 0xD3, \"AMD Radeon Vega 11\"),\n    (0x15DD, 0xD4, \"AMD 15DD\"),\n    (0x15DD, 0xD5, \"AMD Radeon Vega 8\"),\n    (0x15DD, 0xD6, \"AMD Radeon Vega 11\"),\n    (0x15DD, 0xD7, \"AMD Radeon Vega 8\"),\n    (0x15DD, 0xD8, \"AMD Radeon Vega 3\"),\n    (0x15DD, 0xD9, \"AMD Radeon Vega 6\"),\n    (0x15DD, 0xE1, \"AMD Radeon Vega 3\"),\n    (0x15DD, 0xE2, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0x00, \"AMD Radeon RX Vega 8 WS\"),\n    (0x15D8, 0x91, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0x92, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0x93, \"AMD Radeon Vega 1\"),\n    (0x15D8, 0xA1, \"AMD Radeon RX Vega 10\"),\n    (0x15D8, 0xA2, \"AMD Radeon Vega 8\"),\n    (0x15D8, 0xA3, \"AMD Radeon Vega 6\"),\n    (0x15D8, 0xA4, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xB1, \"AMD Radeon Vega 10\"),\n    (0x15D8, 0xB2, \"AMD Radeon Vega 8\"),\n    (0x15D8, 0xB3, \"AMD Radeon Vega 6\"),\n    (0x15D8, 0xB4, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xC1, \"AMD Radeon RX Vega 10\"),\n    (0x15D8, 0xC2, \"AMD Radeon Vega 8\"),\n    (0x15D8, 0xC3, \"AMD Radeon Vega 6\"),\n    (0x15D8, 0xC4, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xC5, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xC8, \"AMD Radeon RX Vega 11\"),\n    (0x15D8, 0xC9, \"AMD Radeon Vega 8\"),\n    (0x15D8, 0xCA, \"AMD Radeon RX Vega 11\"),\n    (0x15D8, 0xCB, \"AMD Radeon Vega 8\"),\n    (0x15D8, 0xCC, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xCE, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xCF, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xD1, \"AMD Radeon Vega 10\"),\n    (0x15D8, 0xD2, \"AMD Radeon Vega 8\"),\n    (0x15D8, 0xD3, \"AMD Radeon Vega 6\"),\n    (0x15D8, 0xD4, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xD8, \"AMD Radeon Vega 11\"),\n    (0x15D8, 0xD9, \"AMD Radeon Vega 8\"),\n    (0x15D8, 0xDA, \"AMD Radeon Vega 11\"),\n    (0x15D8, 0xDB, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xDC, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xDD, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xDE, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xDF, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xE1, \"AMD Radeon RX Vega 11\"),\n    (0x15D8, 0xE2, \"AMD Radeon Vega 9\"),\n    (0x15D8, 0xE3, \"AMD Radeon Vega 3\"),\n    (0x15D8, 0xE4, \"AMD Radeon Vega 3\"),\n    (0x69AF, 0xC0, \"AMD Radeon Pro Vega 20\"),\n    (0x69AF, 0xC7, \"AMD Radeon Pro Vega 16\"),\n    (0x69AF, 0xD7, \"AMD Radeon RX Vega 16\"),\n    (0x66AF, 0xC1, \"AMD Radeon VII\"),\n    (0x66A1, 0x06, \"AMD Radeon Pro VII\"),\n    (0x740C, 0x01, \"AMD Instinct MI250X\"),\n    (0x740F, 0x02, \"AMD Instinct MI210\"),\n    (0x74A1, 0x00, \"AMD Instinct MI300X\"),\n    (0x74A1, 0x01, \"AMD Instinct MI300A\"),\n    (0x7310, 0x00, \"AMD Radeon Pro W5700X\"),\n    (0x7312, 0x00, \"AMD Radeon Pro W5700\"),\n    (0x7319, 0x40, \"AMD Radeon Pro 5700 XT\"),\n    (0x731E, 0xC7, \"AMD Radeon RX 5700B\"),\n    (0x731F, 0xC0, \"AMD Radeon RX 5700 XT 50th Anniversary\"),\n    (0x731F, 0xC1, \"AMD Radeon RX 5700 XT\"),\n    (0x731F, 0xC2, \"AMD Radeon RX 5600M\"),\n    (0x731F, 0xC3, \"AMD Radeon RX 5700M\"),\n    (0x731F, 0xC4, \"AMD Radeon RX 5700\"),\n    (0x731F, 0xC5, \"AMD Radeon RX 5700 XT\"),\n    (0x731F, 0xCA, \"AMD Radeon RX 5600 XT\"),\n    (0x731F, 0xCB, \"AMD Radeon RX 5600\"),\n    (0x7360, 0x41, \"AMD Radeon Pro 5600M\"),\n    (0x7360, 0xC3, \"AMD Radeon Pro V520\"),\n    (0x7362, 0xC3, \"AMD Radeon Pro V520 MxGPU\"),\n    (0x7340, 0x00, \"AMD Radeon Pro W5500X\"),\n    (0x7340, 0x41, \"AMD Radeon Pro 5500 XT\"),\n    (0x7340, 0x47, \"AMD Radeon Pro 5300\"),\n    (0x7340, 0xC1, \"AMD Radeon RX 5500M\"),\n    (0x7340, 0xC3, \"AMD Radeon RX 5300M\"),\n    (0x7340, 0xC5, \"AMD Radeon RX 5500 XT\"),\n    (0x7340, 0xC7, \"AMD Radeon RX 5500\"),\n    (0x7340, 0xCF, \"AMD Radeon RX 5300\"),\n    (0x7341, 0x00, \"AMD Radeon Pro W5500\"),\n    (0x7347, 0x00, \"AMD Radeon Pro W5500M\"),\n    (0x734F, 0x00, \"AMD Radeon Pro W5300M\"),\n    (0x73A5, 0xC0, \"AMD Radeon RX 6950 XT\"),\n    (0x73AF, 0xC0, \"AMD Radeon RX 6900 XT\"),\n    (0x73BF, 0xC0, \"AMD Radeon RX 6900 XT\"),\n    (0x73BF, 0xC1, \"AMD Radeon RX 6800 XT\"),\n    (0x73BF, 0xC3, \"AMD Radeon RX 6800\"),\n    (0x73A1, 0x00, \"AMD Radeon Pro V620\"),\n    (0x73A3, 0x00, \"AMD Radeon Pro W6800\"),\n    (0x73DF, 0xC0, \"AMD Radeon RX 6750 XT\"),\n    (0x73DF, 0xC1, \"AMD Radeon RX 6700 XT\"),\n    (0x73DF, 0xC5, \"AMD Radeon RX 6700 XT\"),\n    (0x73DF, 0xDF, \"AMD Radeon RX 6700\"),\n    (0x73DF, 0xC2, \"AMD Radeon RX 6800M\"),\n    (0x73DF, 0xC3, \"AMD Radeon RX 6800M\"),\n    (0x73DF, 0xCF, \"AMD Radeon RX 6700M\"),\n    (0x73DF, 0xFF, \"AMD Radeon RX 6700\"),\n    (0x73EF, 0xC0, \"AMD Radeon RX 6800S\"),\n    (0x73EF, 0xC1, \"AMD Radeon RX 6650 XT\"),\n    (0x73EF, 0xC2, \"AMD Radeon RX 6700S\"),\n    (0x73EF, 0xC3, \"AMD Radeon RX 6650M\"),\n    (0x73EF, 0xC4, \"AMD Radeon RX 6650M XT\"),\n    (0x73FF, 0xC1, \"AMD Radeon RX 6600 XT\"),\n    (0x73FF, 0xC7, \"AMD Radeon RX 6600\"),\n    (0x73FF, 0xC3, \"AMD Radeon RX 6600M\"),\n    (0x73FF, 0xCB, \"AMD Radeon RX 6600S\"),\n    (0x73E1, 0x00, \"AMD Radeon Pro W6600M\"),\n    (0x73E3, 0x00, \"AMD Radeon Pro W6600\"),\n    (0x7422, 0x00, \"AMD Radeon Pro W6400\"),\n    (0x743F, 0xC1, \"AMD Radeon RX 6500 XT\"),\n    (0x743F, 0xC7, \"AMD Radeon RX 6400\"),\n    (0x743F, 0xD7, \"AMD Radeon RX 6400\"),\n    (0x7421, 0x00, \"AMD Radeon Pro W6500M\"),\n    (0x7423, 0x00, \"AMD Radeon Pro W6300M\"),\n    (0x7423, 0x01, \"AMD Radeon Pro W6300\"),\n    (0x743F, 0xC3, \"AMD Radeon RX 6500M\"),\n    (0x743F, 0xCF, \"AMD Radeon RX 6300M\"),\n    (0x743F, 0xC8, \"AMD Radeon RX 6550M\"),\n    (0x743F, 0xCC, \"AMD Radeon 6550S\"),\n    (0x743F, 0xCE, \"AMD Radeon RX 6450M\"),\n    (0x743F, 0xD3, \"AMD Radeon RX 6550M\"),\n    (0x744C, 0xC8, \"AMD Radeon RX 7900 XTX\"),\n    (0x744C, 0xCC, \"AMD Radeon RX 7900 XT\"),\n    (0x7448, 0x00, \"AMD Radeon Pro W7900\"),\n    (0x745E, 0xCC, \"AMD Radeon Pro W7800\"),\n    (0x747E, 0xC8, \"AMD Radeon RX 7800 XT\"),\n    (0x747E, 0xFF, \"AMD Radeon RX 7700 XT\"),\n    (0x747E, 0xD8, \"AMD Radeon RX 7800M\"),\n    (0x7480, 0xC0, \"AMD Radeon RX 7600 XT\"),\n    (0x7480, 0xCF, \"AMD Radeon RX 7600\"),\n    (0x7480, 0xC1, \"AMD Radeon RX 7700S\"),\n    (0x7480, 0xC3, \"AMD Radeon RX 7600S\"),\n    (0x7480, 0xC7, \"AMD Radeon RX 7600M XT\"),\n    (0x7483, 0xCF, \"AMD Radeon RX 7600M\"),\n    (0x7480, 0x00, \"AMD Radeon Pro W7600\"),\n    (0x7489, 0x00, \"AMD Radeon Pro W7500\"),\n    (0x15BF, 0x00, \"AMD Radeon 780M\"),\n    (0x15BF, 0x01, \"AMD Radeon 760M\"),\n    (0x15BF, 0x02, \"AMD Radeon 780M\"),\n    (0x15BF, 0x03, \"AMD Radeon 760M\"),\n    (0x15BF, 0xC1, \"AMD Radeon 780M\"),\n    (0x15BF, 0xC2, \"AMD Radeon 780M\"),\n    (0x15BF, 0xC3, \"AMD Radeon 760M\"),\n    (0x15BF, 0xC4, \"AMD Radeon 780M\"),\n    (0x15BF, 0xC5, \"AMD Radeon 740M\"),\n    (0x15BF, 0xC6, \"AMD Radeon 780M\"),\n    (0x15BF, 0xC7, \"AMD Radeon 780M\"),\n    (0x15BF, 0xC8, \"AMD Radeon 760M\"),\n    (0x15BF, 0xC9, \"AMD Radeon 780M\"),\n    (0x15BF, 0xCA, \"AMD Radeon 740M\"),\n    (0x15BF, 0xCB, \"AMD Radeon 760M\"),\n    (0x15BF, 0xCC, \"AMD Radeon 740M\"),\n    (0x15BF, 0xCD, \"AMD Radeon 760M\"),\n    (0x15BF, 0xCF, \"AMD Radeon 780M\"),\n    (0x15BF, 0xD0, \"AMD Radeon 780M\"),\n    (0x15BF, 0xD1, \"AMD Radeon 780M\"),\n    (0x15BF, 0xD2, \"AMD Radeon 780M\"),\n    (0x15BF, 0xD3, \"AMD Radeon 780M\"),\n    (0x15BF, 0xD4, \"AMD Radeon 780M\"),\n    (0x15BF, 0xD5, \"AMD Radeon 760M\"),\n    (0x15BF, 0xD6, \"AMD Radeon 760M\"),\n    (0x15BF, 0xD7, \"AMD Radeon 780M\"),\n    (0x15BF, 0xD8, \"AMD Radeon 740M\"),\n    (0x15BF, 0xD9, \"AMD Radeon 780M\"),\n    (0x15BF, 0xDA, \"AMD Radeon 780M\"),\n    (0x15BF, 0xDB, \"AMD Radeon 760M\"),\n    (0x15BF, 0xDC, \"AMD Radeon 760M\"),\n    (0x15BF, 0xDD, \"AMD Radeon 780M\"),\n    (0x15BF, 0xDE, \"AMD Radeon 740M\"),\n    (0x15BF, 0xDF, \"AMD Radeon 760M\"),\n    (0x15BF, 0xF0, \"AMD Radeon 760M\"),\n    (0x1900, 0x01, \"AMD Radeon 780M\"),\n    (0x1900, 0x02, \"AMD Radeon 760M\"),\n    (0x1900, 0x03, \"AMD Radeon 780M\"),\n    (0x1900, 0x04, \"AMD Radeon 760M\"),\n    (0x1900, 0x05, \"AMD Radeon 780M\"),\n    (0x1900, 0x06, \"AMD Radeon 780M\"),\n    (0x1900, 0x07, \"AMD Radeon 760M\"),\n    (0x1900, 0xB0, \"AMD Radeon 780M\"),\n    (0x1900, 0xB1, \"AMD Radeon 780M\"),\n    (0x1900, 0xB2, \"AMD Radeon 780M\"),\n    (0x1900, 0xB3, \"AMD Radeon 780M\"),\n    (0x1900, 0xB4, \"AMD Radeon 780M\"),\n    (0x1900, 0xB5, \"AMD Radeon 780M\"),\n    (0x1900, 0xB6, \"AMD Radeon 780M\"),\n    (0x1900, 0xB7, \"AMD Radeon 760M\"),\n    (0x1900, 0xB8, \"AMD Radeon 760M\"),\n    (0x1900, 0xB9, \"AMD Radeon 780M\"),\n    (0x1900, 0xC0, \"AMD Radeon 780M\"),\n    (0x1900, 0xC1, \"AMD Radeon 760M\"),\n    (0x1900, 0xC2, \"AMD Radeon 780M\"),\n    (0x1900, 0xC3, \"AMD Radeon 760M\"),\n    (0x1900, 0xC4, \"AMD Radeon 780M\"),\n    (0x1900, 0xC5, \"AMD Radeon 780M\"),\n    (0x1900, 0xC6, \"AMD Radeon 760M\"),\n    (0x1900, 0xC7, \"AMD Radeon 780M\"),\n    (0x1900, 0xC8, \"AMD Radeon 760M\"),\n    (0x1900, 0xC9, \"AMD Radeon 780M\"),\n    (0x1900, 0xCA, \"AMD Radeon 760M\"),\n    (0x1900, 0xCB, \"AMD Radeon 780M\"),\n    (0x1900, 0xCC, \"AMD Radeon 780M\"),\n    (0x1900, 0xCD, \"AMD Radeon 760M\"),\n    (0x1900, 0xCE, \"AMD Radeon 780M\"),\n    (0x1900, 0xCF, \"AMD Radeon 760M\"),\n    (0x1900, 0xD0, \"AMD Radeon 780M\"),\n    (0x1900, 0xD1, \"AMD Radeon 760M\"),\n    (0x1900, 0xD2, \"AMD Radeon 780M\"),\n    (0x1900, 0xD3, \"AMD Radeon 760M\"),\n    (0x1900, 0xD4, \"AMD Radeon 780M\"),\n    (0x1900, 0xD5, \"AMD Radeon 780M\"),\n    (0x1900, 0xD6, \"AMD Radeon 760M\"),\n    (0x1900, 0xD7, \"AMD Radeon 780M\"),\n    (0x1900, 0xD8, \"AMD Radeon 760M\"),\n    (0x1900, 0xD9, \"AMD Radeon 780M\"),\n    (0x1900, 0xDA, \"AMD Radeon 760M\"),\n    (0x1900, 0xDB, \"AMD Radeon 780M\"),\n    (0x1900, 0xDC, \"AMD Radeon 780M\"),\n    (0x1900, 0xDD, \"AMD Radeon 760M\"),\n    (0x1900, 0xDE, \"AMD Radeon 780M\"),\n    (0x1900, 0xDF, \"AMD Radeon 760M\"),\n    (0x1900, 0xF0, \"AMD Radeon 780M\"),\n    (0x1900, 0xF1, \"AMD Radeon 780M\"),\n    (0x1900, 0xF2, \"AMD Radeon 780M\"),\n    (0x1901, 0xC8, \"AMD Radeon 740M\"),\n    (0x1901, 0xC9, \"AMD Radeon 740M\"),\n    (0x1901, 0xD5, \"AMD Radeon 740M\"),\n    (0x1901, 0xD6, \"AMD Radeon 740M\"),\n    (0x1901, 0xD7, \"AMD Radeon 740M\"),\n    (0x1901, 0xD8, \"AMD Radeon 740M\"),\n    (0x15C8, 0xC1, \"AMD Radeon 740M\"),\n    (0x15C8, 0xC2, \"AMD Radeon 740M\"),\n    (0x15C8, 0xC3, \"AMD Radeon 740M\"),\n    (0x15C8, 0xC4, \"AMD Radeon 740M\"),\n    (0x15C8, 0xD1, \"AMD Radeon 740M\"),\n    (0x15C8, 0xD2, \"AMD Radeon 740M\"),\n    (0x15C8, 0xD3, \"AMD Radeon 740M\"),\n    (0x15C8, 0xD4, \"AMD Radeon 740M\"),\n    (0x1901, 0xC1, \"AMD Radeon 740M\"),\n    (0x1901, 0xC2, \"AMD Radeon 740M\"),\n    (0x1901, 0xC3, \"AMD Radeon 740M\"),\n    (0x1901, 0xC6, \"AMD Radeon 740M\"),\n    (0x1901, 0xC7, \"AMD Radeon 740M\"),\n    (0x1901, 0xD1, \"AMD Radeon 740M\"),\n    (0x1901, 0xD2, \"AMD Radeon 740M\"),\n    (0x1901, 0xD3, \"AMD Radeon 740M\"),\n    (0x1901, 0xD4, \"AMD Radeon 740M\"),\n    (0x150E, 0xC1, \"AMD Radeon 890M\"),\n    (0x150E, 0xC4, \"AMD Radeon 890M\"),\n    (0x150E, 0xC5, \"AMD Radeon 890M\"),\n    (0x150E, 0xC6, \"AMD Radeon 890M\"),\n    (0x150E, 0xD1, \"AMD Radeon 890M\"),\n    (0x150E, 0xD2, \"AMD Radeon 890M\"),\n    (0x150E, 0xD3, \"AMD Radeon 890M\"),\n    (0x74A9, 0x00, \"AMD Instinct MI300XHF\"),\n    (0x73AE, 0x00, \"AMD Radeon Pro V620 MxGPU\"),\n    (0x73CE, 0xFF, \"AMD Radeon V520 MxGPU\"),\n    (0x7449, 0x00, \"AMD Radeon Pro W7800 48GB\"),\n    (0x744A, 0x00, \"AMD Radeon Pro W7900\"),\n    (0x7480, 0xC2, \"AMD Radeon RX 7650 GRE\"),\n    (0x7481, 0xC7, \"AMD Radeon RX 7600\"),\n    (0x1900, 0xBA, \"AMD Radeon 780M\"),\n    (0x1900, 0xBB, \"AMD Radeon 780M\"),\n    (0x1901, 0xCA, \"AMD Radeon 740M\"),\n    (0x1586, 0xC1, \"AMD Radeon 8060S\"),\n    (0x1586, 0xC2, \"AMD Radeon 8050S\"),\n    (0x1586, 0xC4, \"AMD Radeon 8050S\"),\n    (0x1586, 0xD1, \"AMD Radeon 8060S\"),\n    (0x1586, 0xD2, \"AMD Radeon 8050S\"),\n    (0x1586, 0xD4, \"AMD Radeon 8050S\"),\n    (0x1586, 0xD5, \"AMD Radeon 8040S\"),\n    (0x1114, 0xC2, \"AMD Radeon 860M\"),\n    (0x1114, 0xC3, \"AMD Radeon 840M\"),\n    (0x1114, 0xD2, \"AMD Radeon 860M\"),\n    (0x1114, 0xD3, \"AMD Radeon 840M\"),\n    (0x7550, 0xC0, \"AMD Radeon RX 9070 XT\"),\n    (0x7550, 0xC3, \"AMD Radeon RX 9070\"),\n];\n"
  },
  {
    "path": "src/collection/amd.rs",
    "content": "mod amd_gpu_marketing;\n\nuse std::{\n    fs::{self, read_to_string},\n    num::NonZeroU64,\n    path::{Path, PathBuf},\n    sync::{LazyLock, Mutex},\n    time::{Duration, Instant},\n};\n\nuse nohash::IntMap;\nuse rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};\n\nuse super::linux::utils::is_device_awake;\nuse crate::{\n    app::layout_manager::UsedWidgets,\n    collection::{memory::MemData, processes::Pid},\n};\n\n// TODO: May be able to clean up some of these, Option<Vec> for example is a bit redundant.\npub struct AmdGpuData {\n    pub memory: Option<Vec<(String, MemData)>>,\n    pub procs: Option<(u64, Vec<IntMap<Pid, (u64, u32)>>)>,\n}\n\npub struct AmdGpuMemory {\n    pub total: u64,\n    pub used: u64,\n}\n\n#[derive(Debug, Clone, Default, Eq, PartialEq)]\npub struct AmdGpuProc {\n    pub vram_usage: u64,\n    pub gfx_usage: u64,\n    pub dma_usage: u64,\n    pub enc_usage: u64,\n    pub dec_usage: u64,\n    pub uvd_usage: u64,\n    pub vcn_usage: u64,\n    pub vpe_usage: u64,\n    pub compute_usage: u64,\n}\n\n// needs previous state for usage calculation\nstatic PROC_DATA: LazyLock<Mutex<HashMap<PathBuf, IntMap<Pid, AmdGpuProc>>>> =\n    LazyLock::new(|| Mutex::new(HashMap::default()));\n\nfn get_amd_devs() -> Option<Vec<PathBuf>> {\n    let mut devices = Vec::new();\n\n    // read all PCI devices controlled by the AMDGPU module\n    let Ok(paths) = fs::read_dir(\"/sys/module/amdgpu/drivers/pci:amdgpu\") else {\n        return None;\n    };\n\n    for path in paths {\n        let Ok(path) = path else { continue };\n\n        // test if it has a valid vendor path\n        let device_path = path.path();\n        if !device_path.is_dir() {\n            continue;\n        }\n\n        // Skip if asleep to avoid wakeups.\n        if !is_device_awake(&device_path) {\n            continue;\n        }\n\n        // This will exist for GPUs but not others, this is how we find their kernel\n        // name.\n        let test_path = device_path.join(\"drm\");\n        if test_path.as_path().exists() {\n            devices.push(device_path);\n        }\n    }\n\n    if devices.is_empty() {\n        None\n    } else {\n        Some(devices)\n    }\n}\n\npub fn get_amd_name(device_path: &Path) -> Option<String> {\n    // get revision and device ids from sysfs\n    let rev_path = device_path.join(\"revision\");\n    let dev_path = device_path.join(\"device\");\n\n    if !rev_path.exists() || !dev_path.exists() {\n        return None;\n    }\n\n    // read and remove newlines, 0x0 suffix.\n    let mut rev_data = read_to_string(rev_path).unwrap_or(\"0x00\".to_string());\n    let mut dev_data = read_to_string(dev_path).unwrap_or(\"0x0000\".to_string());\n\n    rev_data = rev_data.trim_end().to_string();\n    dev_data = dev_data.trim_end().to_string();\n\n    if let Some(stripped) = rev_data.strip_prefix(\"0x\") {\n        rev_data = stripped.to_string();\n    }\n\n    if let Some(stripped) = dev_data.strip_prefix(\"0x\") {\n        dev_data = stripped.to_string();\n    }\n\n    let revision_id = u32::from_str_radix(&rev_data, 16).unwrap_or(0);\n    let device_id = u32::from_str_radix(&dev_data, 16).unwrap_or(0);\n\n    if device_id == 0 {\n        return None;\n    }\n\n    // if it exists in our local database, use that name\n    amd_gpu_marketing::AMD_GPU_MARKETING_NAME\n        .iter()\n        .find(|(did, rid, _)| (did, rid) == (&device_id, &revision_id))\n        .map(|tuple| tuple.2.to_string())\n}\n\nfn get_amd_vram(device_path: &Path) -> Option<AmdGpuMemory> {\n    // get vram memory info from sysfs\n    let vram_total_path = device_path.join(\"mem_info_vram_total\");\n    let vram_used_path = device_path.join(\"mem_info_vram_used\");\n\n    let Ok(mut vram_total_data) = read_to_string(vram_total_path) else {\n        return None;\n    };\n    let Ok(mut vram_used_data) = read_to_string(vram_used_path) else {\n        return None;\n    };\n\n    // read and remove newlines\n    vram_total_data = vram_total_data.trim_end().to_string();\n    vram_used_data = vram_used_data.trim_end().to_string();\n\n    let Ok(vram_total) = vram_total_data.parse::<u64>() else {\n        return None;\n    };\n    let Ok(vram_used) = vram_used_data.parse::<u64>() else {\n        return None;\n    };\n\n    Some(AmdGpuMemory {\n        total: vram_total,\n        used: vram_used,\n    })\n}\n\n// from amdgpu_top: https://github.com/Umio-Yasuno/amdgpu_top/blob/c961cf6625c4b6d63fda7f03348323048563c584/crates/libamdgpu_top/src/stat/fdinfo/proc_info.rs#L114\nfn diff_usage(pre: u64, cur: u64, interval: &Duration) -> u64 {\n    use std::ops::Mul;\n\n    let diff_ns = if pre == 0 || cur < pre {\n        return 0;\n    } else {\n        cur.saturating_sub(pre) as u128\n    };\n\n    diff_ns\n        .mul(100)\n        .checked_div(interval.as_nanos())\n        .unwrap_or(0) as u64\n}\n\n// from amdgpu_top: https://github.com/Umio-Yasuno/amdgpu_top/blob/c961cf6625c4b6d63fda7f03348323048563c584/crates/libamdgpu_top/src/stat/fdinfo/proc_info.rs#L13-L27\nfn get_amdgpu_pid_fds(pid: Pid, device_path: Vec<PathBuf>) -> Option<Vec<u32>> {\n    let Ok(fd_list) = fs::read_dir(format!(\"/proc/{pid}/fd/\")) else {\n        return None;\n    };\n\n    let valid_fds: Vec<u32> = fd_list\n        .filter_map(|fd_link| {\n            let dir_entry = fd_link.map(|fd_link| fd_link.path()).ok()?;\n            let link = fs::read_link(&dir_entry).ok()?;\n\n            // e.g. \"/dev/dri/renderD128\" or \"/dev/dri/card0\"\n            if device_path.iter().any(|path| link.starts_with(path)) {\n                dir_entry.file_name()?.to_str()?.parse::<u32>().ok()\n            } else {\n                None\n            }\n        })\n        .collect();\n\n    if valid_fds.is_empty() {\n        None\n    } else {\n        Some(valid_fds)\n    }\n}\n\nfn get_amdgpu_drm(device_path: &Path) -> Option<Vec<PathBuf>> {\n    let mut drm_devices = Vec::new();\n    let drm_root = device_path.join(\"drm\");\n\n    let Ok(drm_paths) = fs::read_dir(drm_root) else {\n        return None;\n    };\n\n    for drm_dir in drm_paths {\n        let Ok(drm_dir) = drm_dir else {\n            continue;\n        };\n\n        // attempt to get the device renderer name\n        let drm_name = drm_dir.file_name();\n        let Some(drm_name) = drm_name.to_str() else {\n            continue;\n        };\n\n        // construct driver device path if valid\n        if !drm_name.starts_with(\"card\") && !drm_name.starts_with(\"render\") {\n            continue;\n        }\n\n        drm_devices.push(PathBuf::from(format!(\"/dev/dri/{drm_name}\")));\n    }\n\n    if drm_devices.is_empty() {\n        None\n    } else {\n        Some(drm_devices)\n    }\n}\n\nfn get_amd_fdinfo(device_path: &Path) -> Option<IntMap<Pid, AmdGpuProc>> {\n    let mut fdinfo = IntMap::default();\n\n    let drm_paths = get_amdgpu_drm(device_path)?;\n\n    let Ok(proc_dir) = fs::read_dir(\"/proc\") else {\n        return None;\n    };\n\n    let pids: Vec<Pid> = proc_dir\n        .filter_map(|dir_entry| {\n            // check if pid is valid\n            let dir_entry = dir_entry.ok()?;\n            let metadata = dir_entry.metadata().ok()?;\n\n            if !metadata.is_dir() {\n                return None;\n            }\n\n            let pid = dir_entry.file_name().to_str()?.parse::<Pid>().ok()?;\n\n            // skip init process\n            if pid == 1 {\n                return None;\n            }\n\n            Some(pid)\n        })\n        .collect();\n\n    for pid in pids {\n        // collect file descriptors that point to our device renderers\n        let Some(fds) = get_amdgpu_pid_fds(pid, drm_paths.clone()) else {\n            continue;\n        };\n\n        let mut usage: AmdGpuProc = Default::default();\n\n        let mut observed_ids: HashSet<usize> = HashSet::default();\n\n        for fd in fds {\n            let fdinfo_path = format!(\"/proc/{pid}/fdinfo/{fd}\");\n            let Ok(fdinfo_data) = read_to_string(fdinfo_path) else {\n                continue;\n            };\n\n            let mut fdinfo_lines = fdinfo_data\n                .lines()\n                .skip_while(|l| !l.starts_with(\"drm-client-id\"));\n            if let Some(id) = fdinfo_lines.next().and_then(|fdinfo_line| {\n                const LEN: usize = \"drm-client-id:\\t\".len();\n                fdinfo_line.get(LEN..)?.parse().ok()\n            }) {\n                if !observed_ids.insert(id) {\n                    continue;\n                }\n            } else {\n                continue;\n            }\n\n            for fdinfo_line in fdinfo_lines {\n                let Some(fdinfo_separator_index) = fdinfo_line.find(':') else {\n                    continue;\n                };\n\n                let (fdinfo_keyword, mut fdinfo_value) =\n                    fdinfo_line.split_at(fdinfo_separator_index);\n                fdinfo_value = &fdinfo_value[1..];\n\n                fdinfo_value = fdinfo_value.trim();\n                if let Some(fdinfo_value_space_index) = fdinfo_value.find(' ') {\n                    fdinfo_value = &fdinfo_value[..fdinfo_value_space_index];\n                };\n\n                let Ok(fdinfo_value_num) = fdinfo_value.parse::<u64>() else {\n                    continue;\n                };\n\n                match fdinfo_keyword {\n                    \"drm-engine-gfx\" => usage.gfx_usage += fdinfo_value_num,\n                    \"drm-engine-dma\" => usage.dma_usage += fdinfo_value_num,\n                    \"drm-engine-dec\" => usage.dec_usage += fdinfo_value_num,\n                    \"drm-engine-enc\" => usage.enc_usage += fdinfo_value_num,\n                    \"drm-engine-enc_1\" => usage.uvd_usage += fdinfo_value_num,\n                    \"drm-engine-jpeg\" => usage.vcn_usage += fdinfo_value_num,\n                    \"drm-engine-vpe\" => usage.vpe_usage += fdinfo_value_num,\n                    \"drm-engine-compute\" => usage.compute_usage += fdinfo_value_num,\n                    \"drm-memory-vram\" => usage.vram_usage += fdinfo_value_num << 10, // KiB -> B\n                    _ => {}\n                };\n            }\n        }\n\n        if usage != Default::default() {\n            fdinfo.insert(pid, usage);\n        }\n    }\n\n    Some(fdinfo)\n}\n\npub fn get_amd_vecs(widgets_to_harvest: &UsedWidgets, prev_time: Instant) -> Option<AmdGpuData> {\n    let device_path_list = get_amd_devs()?;\n    let interval = Instant::now().duration_since(prev_time);\n    let num_gpu = device_path_list.len();\n    let mut mem_vec = Vec::with_capacity(num_gpu);\n    let mut proc_vec = Vec::with_capacity(num_gpu);\n    let mut total_mem = 0;\n\n    for device_path in device_path_list {\n        let device_name = get_amd_name(&device_path)\n            .unwrap_or(amd_gpu_marketing::AMDGPU_DEFAULT_NAME.to_string());\n\n        if let Some(mem) = get_amd_vram(&device_path) {\n            if widgets_to_harvest.use_mem {\n                if let Some(total_bytes) = NonZeroU64::new(mem.total) {\n                    mem_vec.push((\n                        device_name.clone(),\n                        MemData {\n                            total_bytes,\n                            used_bytes: mem.used,\n                        },\n                    ));\n                }\n            }\n\n            total_mem += mem.total\n        }\n\n        if widgets_to_harvest.use_proc {\n            if let Some(procs) = get_amd_fdinfo(&device_path) {\n                let mut proc_info = PROC_DATA.lock().expect(\"mutex is poisoned\");\n                let prev_fdinfo = proc_info.entry(device_path).or_default();\n\n                let mut procs_map = IntMap::default();\n                for (proc_pid, proc_usage) in procs {\n                    if let Some(prev_usage) = prev_fdinfo.get_mut(&proc_pid) {\n                        // calculate deltas\n                        let gfx_usage =\n                            diff_usage(prev_usage.gfx_usage, proc_usage.gfx_usage, &interval);\n                        let dma_usage =\n                            diff_usage(prev_usage.dma_usage, proc_usage.dma_usage, &interval);\n                        let enc_usage =\n                            diff_usage(prev_usage.enc_usage, proc_usage.enc_usage, &interval);\n                        let dec_usage =\n                            diff_usage(prev_usage.dec_usage, proc_usage.dec_usage, &interval);\n                        let uvd_usage =\n                            diff_usage(prev_usage.uvd_usage, proc_usage.uvd_usage, &interval);\n                        let vcn_usage =\n                            diff_usage(prev_usage.vcn_usage, proc_usage.vcn_usage, &interval);\n                        let vpe_usage =\n                            diff_usage(prev_usage.vpe_usage, proc_usage.vpe_usage, &interval);\n\n                        // combined usage\n                        let gpu_util_wide = gfx_usage\n                            + dma_usage\n                            + enc_usage\n                            + dec_usage\n                            + uvd_usage\n                            + vcn_usage\n                            + vpe_usage;\n\n                        let gpu_util: u32 = gpu_util_wide.try_into().unwrap_or(0);\n\n                        if gpu_util > 0 || proc_usage.vram_usage > 0 {\n                            procs_map.insert(proc_pid, (proc_usage.vram_usage, gpu_util));\n                        }\n\n                        *prev_usage = proc_usage;\n                    } else {\n                        prev_fdinfo.insert(proc_pid, proc_usage);\n                    }\n                }\n\n                if !procs_map.is_empty() {\n                    proc_vec.push(procs_map);\n                }\n            }\n        }\n    }\n\n    Some(AmdGpuData {\n        memory: (!mem_vec.is_empty()).then_some(mem_vec),\n        procs: (!proc_vec.is_empty()).then_some((total_mem, proc_vec)),\n    })\n}\n"
  },
  {
    "path": "src/collection/batteries.rs",
    "content": "//! Uses the battery crate.\n//!\n//! Covers battery usage for:\n//! - Linux 2.6.39+\n//! - MacOS 10.10+\n//! - iOS\n//! - Windows 7+\n//! - FreeBSD\n//! - DragonFlyBSD\n//!\n//! For more information, refer to the [starship_battery](https://github.com/starship/rust-battery) repo/docs.\n\nuse starship_battery::{\n    Battery, Manager, State,\n    units::{power::watt, ratio::percent, time::second},\n};\n\n/// Battery state.\n#[derive(Debug, Clone)]\npub enum BatteryState {\n    Charging {\n        /// Time to full in seconds.\n        time_to_full: Option<u32>,\n    },\n    Discharging {\n        /// Time to empty in seconds.\n        time_to_empty: Option<u32>,\n    },\n    Empty,\n    Full,\n    Unknown,\n}\n\nimpl BatteryState {\n    /// Return the string representation.\n    pub fn as_str(&self) -> &'static str {\n        match self {\n            BatteryState::Charging { .. } => \"Charging\",\n            BatteryState::Discharging { .. } => \"Discharging\",\n            BatteryState::Empty => \"Empty\",\n            BatteryState::Full => \"Full\",\n            BatteryState::Unknown => \"Unknown\",\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct BatteryData {\n    /// Current charge percent.\n    pub charge_percent: f64,\n    /// Power consumption, in watts.\n    pub power_consumption: f64,\n    /// Reported battery health.\n    pub health_percent: f64,\n    /// The current battery \"state\" (e.g. is it full, charging, etc.).\n    pub state: BatteryState,\n}\n\nimpl BatteryData {\n    pub fn watt_consumption(&self) -> String {\n        format!(\"{:.2}W\", self.power_consumption)\n    }\n\n    pub fn health(&self) -> String {\n        format!(\"{:.2}%\", self.health_percent)\n    }\n}\n\npub fn refresh_batteries(manager: &Manager, batteries: &mut [Battery]) -> Vec<BatteryData> {\n    batteries\n        .iter_mut()\n        .filter_map(|battery| {\n            if manager.refresh(battery).is_ok() {\n                Some(BatteryData {\n                    charge_percent: f64::from(battery.state_of_charge().get::<percent>()),\n                    power_consumption: f64::from(battery.energy_rate().get::<watt>()),\n                    health_percent: f64::from(battery.state_of_health().get::<percent>()),\n                    state: match battery.state() {\n                        State::Unknown => BatteryState::Unknown,\n                        State::Charging => BatteryState::Charging {\n                            time_to_full: {\n                                let optional_time = battery.time_to_full();\n                                optional_time.map(|time| f64::from(time.get::<second>()) as u32)\n                            },\n                        },\n                        State::Discharging => BatteryState::Discharging {\n                            time_to_empty: {\n                                let optional_time = battery.time_to_empty();\n                                optional_time.map(|time| f64::from(time.get::<second>()) as u32)\n                            },\n                        },\n                        State::Empty => BatteryState::Empty,\n                        State::Full => BatteryState::Full,\n                    },\n                })\n            } else {\n                None\n            }\n        })\n        .collect::<Vec<_>>()\n}\n"
  },
  {
    "path": "src/collection/cpu/sysinfo.rs",
    "content": "//! CPU stats through sysinfo.\n//! Supports FreeBSD.\n\nuse sysinfo::System;\n\nuse super::{CpuData, CpuDataType, CpuHarvest};\nuse crate::collection::error::CollectionResult;\n\npub fn get_cpu_data_list(sys: &System, show_average_cpu: bool) -> CollectionResult<CpuHarvest> {\n    let mut cpus = vec![];\n\n    if show_average_cpu {\n        cpus.push(CpuData {\n            data_type: CpuDataType::Avg,\n            usage: sys.global_cpu_usage(),\n        })\n    }\n\n    cpus.extend(\n        sys.cpus()\n            .iter()\n            .enumerate()\n            .map(|(i, cpu)| CpuData {\n                data_type: CpuDataType::Cpu(i),\n                usage: cpu.cpu_usage(),\n            })\n            .collect::<Vec<_>>(),\n    );\n\n    Ok(cpus)\n}\n\n#[cfg(unix)]\npub(crate) fn get_load_avg() -> crate::collection::cpu::LoadAvgHarvest {\n    // The API for sysinfo apparently wants you to call it like this, rather than\n    // using a &System.\n    let sysinfo::LoadAvg { one, five, fifteen } = sysinfo::System::load_average();\n\n    [one as f32, five as f32, fifteen as f32]\n}\n"
  },
  {
    "path": "src/collection/cpu.rs",
    "content": "//! Data collection for CPU usage and load average.\n\npub mod sysinfo;\npub use self::sysinfo::*;\n\npub type LoadAvgHarvest = [f32; 3];\n\n#[derive(Debug, Clone, Copy)]\npub enum CpuDataType {\n    Avg,\n    Cpu(usize),\n}\n\n#[derive(Debug, Clone)]\npub struct CpuData {\n    pub data_type: CpuDataType,\n    pub usage: f32,\n}\n\npub type CpuHarvest = Vec<CpuData>;\n"
  },
  {
    "path": "src/collection/disks/freebsd.rs",
    "content": "//! Disk stats for FreeBSD.\n\nuse std::io;\n\nuse rustc_hash::FxHashMap as HashMap;\nuse serde::Deserialize;\n\nuse super::{DiskHarvest, IoHarvest, keep_disk_entry};\nuse crate::collection::{DataCollector, deserialize_xo, disks::IoData, error::CollectionResult};\n\n#[derive(Deserialize, Debug, Default)]\n#[serde(rename_all = \"kebab-case\")]\nstruct StorageSystemInformation {\n    filesystem: Vec<FileSystem>,\n}\n\n#[derive(Deserialize, Debug)]\n#[serde(rename_all = \"kebab-case\")]\nstruct FileSystem {\n    name: String,\n    total_blocks: u64,\n    used_blocks: u64,\n    available_blocks: u64,\n    mounted_on: String,\n}\n\npub fn get_io_usage() -> CollectionResult<IoHarvest> {\n    // TODO: Should this (and other I/O collectors) fail fast? In general, should\n    // collection ever fail fast?\n    #[cfg_attr(not(feature = \"zfs\"), expect(unused_mut))]\n    let mut io_harvest: HashMap<String, Option<IoData>> =\n        get_disk_info().map(|storage_system_information| {\n            storage_system_information\n                .filesystem\n                .into_iter()\n                .map(|disk| (disk.name, None))\n                .collect()\n        })?;\n\n    #[cfg(feature = \"zfs\")]\n    {\n        use crate::collection::disks::zfs_io_counters;\n        if let Ok(zfs_io) = zfs_io_counters::zfs_io_stats() {\n            for io in zfs_io.into_iter() {\n                let mount_point = io.device_name().to_string_lossy();\n                io_harvest.insert(\n                    mount_point.to_string(),\n                    Some(IoData {\n                        read_bytes: io.read_bytes(),\n                        write_bytes: io.write_bytes(),\n                    }),\n                );\n            }\n        }\n    }\n    Ok(io_harvest)\n}\n\npub fn get_disk_usage(collector: &DataCollector) -> CollectionResult<Vec<DiskHarvest>> {\n    let disk_filter = &collector.filters.disk_filter;\n    let mount_filter = &collector.filters.mount_filter;\n    let vec_disks: Vec<DiskHarvest> = get_disk_info().map(|storage_system_information| {\n        storage_system_information\n            .filesystem\n            .into_iter()\n            .filter_map(|disk| {\n                if keep_disk_entry(&disk.name, &disk.mounted_on, disk_filter, mount_filter) {\n                    Some(DiskHarvest {\n                        free_space: Some(disk.available_blocks * 1024),\n                        used_space: Some(disk.used_blocks * 1024),\n                        total_space: Some(disk.total_blocks * 1024),\n                        mount_point: disk.mounted_on,\n                        name: disk.name,\n                    })\n                } else {\n                    None\n                }\n            })\n            .collect()\n    })?;\n\n    Ok(vec_disks)\n}\n\nfn get_disk_info() -> io::Result<StorageSystemInformation> {\n    // TODO: Ideally we don't have to shell out to a new program.\n    let output = std::process::Command::new(\"df\")\n        .args([\"--libxo\", \"json\", \"-k\", \"-t\", \"ufs,msdosfs,zfs\"])\n        .output()?;\n    deserialize_xo(\"storage-system-information\", &output.stdout)\n}\n"
  },
  {
    "path": "src/collection/disks/io_counters.rs",
    "content": "use std::ffi::OsStr;\n\n#[derive(Debug, Default)]\npub struct IoCounters {\n    name: String,\n    read_bytes: u64,\n    write_bytes: u64,\n}\n\nimpl IoCounters {\n    pub fn new(name: String, read_bytes: u64, write_bytes: u64) -> Self {\n        Self {\n            name,\n            read_bytes,\n            write_bytes,\n        }\n    }\n\n    pub(crate) fn device_name(&self) -> &OsStr {\n        OsStr::new(&self.name)\n    }\n\n    pub(crate) fn read_bytes(&self) -> u64 {\n        self.read_bytes\n    }\n\n    pub(crate) fn write_bytes(&self) -> u64 {\n        self.write_bytes\n    }\n}\n"
  },
  {
    "path": "src/collection/disks/other.rs",
    "content": "//! Fallback disk info using sysinfo.\n\nuse super::{DiskHarvest, keep_disk_entry};\nuse crate::collection::DataCollector;\n\npub(crate) fn get_disk_usage(collector: &DataCollector) -> anyhow::Result<Vec<DiskHarvest>> {\n    let disks = &collector.sys.disks;\n    let disk_filter = &collector.filters.disk_filter;\n    let mount_filter = &collector.filters.mount_filter;\n\n    Ok(disks\n        .iter()\n        .filter_map(|disk| {\n            let name = {\n                let name = disk.name();\n\n                if name.is_empty() {\n                    \"No Name\".to_string()\n                } else {\n                    name.to_os_string()\n                        .into_string()\n                        .unwrap_or_else(|_| \"Name Unavailable\".to_string())\n                }\n            };\n\n            let mount_point = disk\n                .mount_point()\n                .as_os_str()\n                .to_os_string()\n                .into_string()\n                .unwrap_or_else(|_| \"Mount Unavailable\".to_string());\n\n            if keep_disk_entry(&name, &mount_point, disk_filter, mount_filter) {\n                let free_space = disk.available_space();\n                let total_space = disk.total_space();\n                let used_space = total_space - free_space;\n\n                Some(DiskHarvest {\n                    name,\n                    mount_point,\n                    free_space: Some(free_space),\n                    used_space: Some(used_space),\n                    total_space: Some(total_space),\n                })\n            } else {\n                None\n            }\n        })\n        .collect())\n}\n"
  },
  {
    "path": "src/collection/disks/unix/file_systems.rs",
    "content": "use std::str::FromStr;\n\nuse crate::multi_eq_ignore_ascii_case;\n\n/// Known filesystems. Original list from\n/// [heim](https://github.com/heim-rs/heim/blob/master/heim-disk/src/filesystem.rs).\n///\n/// All physical filesystems should have their own enum element and all virtual\n/// filesystems will go into the [`FileSystem::Other`] element.\n#[derive(Debug, Eq, PartialEq, Hash, Clone)]\n#[non_exhaustive]\npub enum FileSystem {\n    /// ext2 (<https://en.wikipedia.org/wiki/Ext2>)\n    Ext2,\n\n    /// ext3 (<https://en.wikipedia.org/wiki/Ext3>)\n    Ext3,\n\n    /// ext4 (<https://en.wikipedia.org/wiki/Ext4>)\n    Ext4,\n\n    /// FAT (<https://en.wikipedia.org/wiki/File_Allocation_Table>)\n    VFat,\n\n    /// exFAT (<https://en.wikipedia.org/wiki/ExFAT>)\n    ExFat,\n\n    /// F2FS (<https://en.wikipedia.org/wiki/F2FS>)\n    F2fs,\n\n    /// NTFS (<https://en.wikipedia.org/wiki/NTFS>)\n    Ntfs,\n\n    /// ZFS (<https://en.wikipedia.org/wiki/ZFS>)\n    Zfs,\n\n    /// HFS (<https://en.wikipedia.org/wiki/Hierarchical_File_System>)\n    Hfs,\n\n    /// HFS+ (<https://en.wikipedia.org/wiki/HFS_Plus>)\n    HfsPlus,\n\n    /// JFS (<https://en.wikipedia.org/wiki/JFS_(file_system)>)\n    Jfs,\n\n    /// ReiserFS 3 (<https://en.wikipedia.org/wiki/ReiserFS>)\n    Reiser3,\n\n    /// ReiserFS 4 (<https://en.wikipedia.org/wiki/Reiser4>)\n    Reiser4,\n\n    /// Btrfs (<https://en.wikipedia.org/wiki/Btrfs>)\n    Btrfs,\n\n    /// Bcachefs (<https://en.wikipedia.org/wiki/Bcachefs>)\n    Bcachefs,\n\n    /// MINIX FS (<https://en.wikipedia.org/wiki/MINIX_file_system>)\n    Minix,\n\n    /// NILFS (<https://en.wikipedia.org/wiki/NILFS>)\n    Nilfs,\n\n    /// XFS (<https://en.wikipedia.org/wiki/XFS>)\n    Xfs,\n\n    /// APFS (<https://en.wikipedia.org/wiki/Apple_File_System>)\n    Apfs,\n\n    /// FUSE (<https://en.wikipedia.org/wiki/Filesystem_in_Userspace>)\n    FuseBlk,\n\n    /// Some unspecified filesystem.\n    Other(String),\n}\n\nimpl FileSystem {\n    /// Checks if filesystem is used for a physical devices.\n    #[inline]\n    pub fn is_physical(&self) -> bool {\n        !self.is_virtual()\n    }\n\n    /// Checks if filesystem is used for a virtual devices (such as `tmpfs` or\n    /// `smb` mounts).\n    #[inline]\n    pub fn is_virtual(&self) -> bool {\n        matches!(self, FileSystem::Other(..))\n    }\n\n    #[expect(dead_code)]\n    #[inline]\n    /// Returns a string literal identifying this filesystem.\n    pub fn as_str(&self) -> &str {\n        match self {\n            FileSystem::Ext2 => \"ext2\",\n            FileSystem::Ext3 => \"ext3\",\n            FileSystem::Ext4 => \"ext4\",\n            FileSystem::VFat => \"vfat\",\n            FileSystem::Ntfs => \"ntfs\",\n            FileSystem::Zfs => \"zfs\",\n            FileSystem::Hfs => \"hfs\",\n            FileSystem::Reiser3 => \"reiserfs\",\n            FileSystem::Reiser4 => \"reiser4\",\n            FileSystem::FuseBlk => \"fuseblk\",\n            FileSystem::ExFat => \"exfat\",\n            FileSystem::F2fs => \"f2fs\",\n            FileSystem::HfsPlus => \"hfs+\",\n            FileSystem::Jfs => \"jfs\",\n            FileSystem::Btrfs => \"btrfs\",\n            FileSystem::Bcachefs => \"bcachefs\",\n            FileSystem::Minix => \"minix\",\n            FileSystem::Nilfs => \"nilfs\",\n            FileSystem::Xfs => \"xfs\",\n            FileSystem::Apfs => \"apfs\",\n            FileSystem::Other(string) => string.as_str(),\n        }\n    }\n}\n\nimpl FromStr for FileSystem {\n    type Err = anyhow::Error;\n\n    #[inline]\n    fn from_str(s: &str) -> anyhow::Result<Self> {\n        // Done like this as `eq_ignore_ascii_case` avoids a string allocation.\n        Ok(if s.eq_ignore_ascii_case(\"ext2\") {\n            FileSystem::Ext2\n        } else if s.eq_ignore_ascii_case(\"ext3\") {\n            FileSystem::Ext3\n        } else if s.eq_ignore_ascii_case(\"ext4\") {\n            FileSystem::Ext4\n        } else if multi_eq_ignore_ascii_case!(s, \"msdos\" | \"vfat\") {\n            FileSystem::VFat\n        } else if multi_eq_ignore_ascii_case!(s, \"ntfs3\" | \"ntfs\") {\n            FileSystem::Ntfs\n        } else if s.eq_ignore_ascii_case(\"zfs\") {\n            FileSystem::Zfs\n        } else if s.eq_ignore_ascii_case(\"hfs\") {\n            FileSystem::Hfs\n        } else if s.eq_ignore_ascii_case(\"reiserfs\") {\n            FileSystem::Reiser3\n        } else if s.eq_ignore_ascii_case(\"reiser4\") {\n            FileSystem::Reiser4\n        } else if s.eq_ignore_ascii_case(\"exfat\") {\n            FileSystem::ExFat\n        } else if s.eq_ignore_ascii_case(\"f2fs\") {\n            FileSystem::F2fs\n        } else if s.eq_ignore_ascii_case(\"hfsplus\") {\n            FileSystem::HfsPlus\n        } else if s.eq_ignore_ascii_case(\"jfs\") {\n            FileSystem::Jfs\n        } else if s.eq_ignore_ascii_case(\"btrfs\") {\n            FileSystem::Btrfs\n        } else if s.eq_ignore_ascii_case(\"bcachefs\") {\n            FileSystem::Bcachefs\n        } else if s.eq_ignore_ascii_case(\"minix\") {\n            FileSystem::Minix\n        } else if multi_eq_ignore_ascii_case!(s, \"nilfs\" | \"nilfs2\") {\n            FileSystem::Nilfs\n        } else if s.eq_ignore_ascii_case(\"xfs\") {\n            FileSystem::Xfs\n        } else if s.eq_ignore_ascii_case(\"apfs\") {\n            FileSystem::Apfs\n        } else if s.eq_ignore_ascii_case(\"fuseblk\") {\n            FileSystem::FuseBlk\n        } else {\n            FileSystem::Other(s.to_string())\n        })\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use std::str::FromStr;\n\n    use super::FileSystem;\n\n    #[test]\n    fn file_system_from_str() {\n        // Something supported\n        assert_eq!(FileSystem::from_str(\"ext4\").unwrap(), FileSystem::Ext4);\n        assert_eq!(FileSystem::from_str(\"msdos\").unwrap(), FileSystem::VFat);\n        assert_eq!(FileSystem::from_str(\"vfat\").unwrap(), FileSystem::VFat);\n\n        // Something unsupported\n        assert_eq!(\n            FileSystem::from_str(\"this does not exist\").unwrap(),\n            FileSystem::Other(\"this does not exist\".to_owned())\n        );\n    }\n}\n"
  },
  {
    "path": "src/collection/disks/unix/linux/counters.rs",
    "content": "//! Based on [heim's implementation](https://github.com/heim-rs/heim/blob/master/heim-disk/src/sys/linux/counters.rs).\n\nuse std::{\n    fs::File,\n    io::{self, BufRead, BufReader},\n    num::ParseIntError,\n    str::FromStr,\n};\n\nuse crate::collection::disks::IoCounters;\n\n/// Copied from the `psutil` sources:\n///\n/// \"man iostat\" states that sectors are equivalent with blocks and have\n/// a size of 512 bytes. Despite this value can be queried at runtime\n/// via /sys/block/{DISK}/queue/hw_sector_size and results may vary\n/// between 1k, 2k, or 4k... 512 appears to be a magic constant used\n/// throughout Linux source code:\n/// * <https://stackoverflow.com/a/38136179/376587>\n/// * <https://lists.gt.net/linux/kernel/2241060>\n/// * <https://github.com/giampaolo/psutil/issues/1305>\n/// * <https://github.com/torvalds/linux/blob/4f671fe2f9523a1ea206f63fe60a7c7b3a56d5c7/include/linux/bio.h#L99>\n/// * <https://lkml.org/lkml/2015/8/17/234>\nconst DISK_SECTOR_SIZE: u64 = 512;\n\nimpl FromStr for IoCounters {\n    type Err = anyhow::Error;\n\n    /// Converts a `&str` to an [`IoCounters`].\n    ///\n    /// Follows the format used in Linux 2.6+. Note that this completely ignores\n    /// the following stats:\n    /// - Discard stats from 4.18+\n    /// - Flush stats from 5.5+\n    ///\n    /// <https://www.kernel.org/doc/Documentation/iostats.txt>\n    /// <https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats>\n    fn from_str(s: &str) -> anyhow::Result<IoCounters> {\n        fn next_part<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Result<&'a str, io::Error> {\n            iter.next()\n                .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))\n        }\n\n        fn next_part_to_u64<'a>(iter: &mut impl Iterator<Item = &'a str>) -> anyhow::Result<u64> {\n            next_part(iter)?\n                .parse()\n                .map_err(|err: ParseIntError| err.into())\n        }\n\n        // Skip the major and minor numbers.\n        let mut parts = s.split_whitespace().skip(2);\n\n        let name = next_part(&mut parts)?.to_string();\n\n        // Skip read count, read merged count.\n        let mut parts = parts.skip(2);\n        let read_bytes = next_part_to_u64(&mut parts)? * DISK_SECTOR_SIZE;\n\n        // Skip read time seconds, write count, and write merged count.\n        let mut parts = parts.skip(3);\n        let write_bytes = next_part_to_u64(&mut parts)? * DISK_SECTOR_SIZE;\n\n        Ok(IoCounters::new(name, read_bytes, write_bytes))\n    }\n}\n\n/// Returns an iterator of disk I/O stats. Pulls data from `/proc/diskstats`.\npub fn io_stats() -> anyhow::Result<Vec<IoCounters>> {\n    const PROC_DISKSTATS: &str = \"/proc/diskstats\";\n\n    let mut results = vec![];\n    let mut reader = BufReader::new(File::open(PROC_DISKSTATS)?);\n    let mut line = String::new();\n\n    // This saves us from doing a string allocation on each iteration compared to\n    // `lines()`.\n    while let Ok(bytes) = reader.read_line(&mut line) {\n        if bytes > 0 {\n            if let Ok(counters) = IoCounters::from_str(&line) {\n                results.push(counters);\n            }\n            line.clear();\n        } else {\n            break;\n        }\n    }\n\n    #[cfg(feature = \"zfs\")]\n    {\n        use crate::collection::disks::zfs_io_counters;\n        if let Ok(mut zfs_io) = zfs_io_counters::zfs_io_stats() {\n            results.append(&mut zfs_io);\n        }\n    }\n\n    Ok(results)\n}\n"
  },
  {
    "path": "src/collection/disks/unix/linux/mod.rs",
    "content": "mod counters;\nmod partition;\n\npub use counters::*;\npub(crate) use partition::*;\n"
  },
  {
    "path": "src/collection/disks/unix/linux/partition.rs",
    "content": "//! Implementation based on [heim's](https://github.com/heim-rs/heim)\n//! Unix disk usage.\n\nuse std::{\n    ffi::CString,\n    fs::File,\n    io::{self, BufRead, BufReader},\n    mem,\n    path::{Path, PathBuf},\n    str::FromStr,\n};\n\nuse anyhow::bail;\n\nuse crate::collection::disks::unix::{FileSystem, Usage};\n\n/// Representation of partition details. Based on [`heim`](https://github.com/heim-rs/heim/tree/master).\npub(crate) struct Partition {\n    device: Option<String>,\n    mount_point: PathBuf,\n    fs_type: FileSystem,\n}\n\nimpl Partition {\n    /// Returns the device name, if there is one.\n    #[inline]\n    pub fn device(&self) -> Option<&str> {\n        self.device.as_deref()\n    }\n\n    /// Returns the mount point for this partition.\n    #[inline]\n    pub fn mount_point(&self) -> &Path {\n        self.mount_point.as_path()\n    }\n\n    /// Returns the [`FileSystem`] of this partition.\n    #[inline]\n    pub fn fs_type(&self) -> &FileSystem {\n        &self.fs_type\n    }\n\n    /// Returns the device name for the partition.\n    pub fn get_device_name(&self) -> String {\n        if let Some(device) = self.device() {\n            // See if this disk is actually mounted elsewhere on Linux. This is a workaround\n            // properly map I/O in some cases (i.e. disk encryption, https://github.com/ClementTsang/bottom/issues/419).\n            if let Ok(path) = std::fs::read_link(device) {\n                if path.is_absolute() {\n                    path.into_os_string()\n                        .into_string()\n                        .unwrap_or_else(|_| \"Name Unavailable\".to_string())\n                } else {\n                    let mut combined_path = PathBuf::new();\n                    combined_path.push(device);\n                    combined_path.pop(); // Pop the current file...\n                    combined_path.push(path);\n\n                    if let Ok(canon_path) = std::fs::canonicalize(combined_path) {\n                        // Resolve the local path into an absolute one...\n                        canon_path\n                            .into_os_string()\n                            .into_string()\n                            .unwrap_or_else(|_| \"Name Unavailable\".to_string())\n                    } else {\n                        device.to_owned()\n                    }\n                }\n            } else {\n                device.to_owned()\n            }\n        } else {\n            \"Name Unavailable\".to_string()\n        }\n    }\n\n    /// Returns the usage stats for this partition.\n    pub fn usage(&self) -> anyhow::Result<Usage> {\n        // TODO: This might be unoptimal.\n        let path = self\n            .mount_point\n            .to_str()\n            .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))\n            .and_then(|string| {\n                CString::new(string).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))\n            })\n            .map_err(|e| anyhow::anyhow!(\"invalid path: {e:?}\"))?;\n\n        let mut vfs = mem::MaybeUninit::<libc::statvfs>::uninit();\n\n        // SAFETY: libc call, `path` is a valid C string and buf is a valid pointer to\n        // write to.\n        let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) };\n\n        if result == 0 {\n            // SAFETY: If result is 0, it succeeded, and vfs should be non-null.\n            let vfs = unsafe { vfs.assume_init() };\n            Ok(Usage::new(vfs))\n        } else {\n            Err(anyhow::anyhow!(\n                \"statvfs had an issue getting info from {path:?}\"\n            ))\n        }\n    }\n}\n\nfn fix_mount_point(s: &str) -> String {\n    const ESCAPED_BACKSLASH: &str = \"\\\\134\";\n    const ESCAPED_SPACE: &str = \"\\\\040\";\n    const ESCAPED_TAB: &str = \"\\\\011\";\n    const ESCAPED_NEWLINE: &str = \"\\\\012\";\n\n    s.replace(ESCAPED_BACKSLASH, \"\\\\\")\n        .replace(ESCAPED_SPACE, \" \")\n        .replace(ESCAPED_TAB, \"\\t\")\n        .replace(ESCAPED_NEWLINE, \"\\n\")\n}\n\nimpl FromStr for Partition {\n    type Err = anyhow::Error;\n\n    fn from_str(line: &str) -> anyhow::Result<Partition> {\n        // Example: `/dev/sda3 /home ext4 rw,relatime,data=ordered 0 0`\n        let mut parts = line.trim_start().splitn(5, ' ');\n\n        let device = match parts.next() {\n            Some(\"none\") => None,\n            Some(device) => Some(device.to_string()),\n            None => {\n                bail!(\"missing device\");\n            }\n        };\n\n        let mount_point = match parts.next() {\n            Some(mount_point) => PathBuf::from(fix_mount_point(mount_point)),\n            None => {\n                bail!(\"missing mount point\");\n            }\n        };\n        let fs_type = match parts.next() {\n            Some(fs) => FileSystem::from_str(fs)?,\n            _ => {\n                bail!(\"missing filesystem type\");\n            }\n        };\n\n        Ok(Partition {\n            device,\n            mount_point,\n            fs_type,\n        })\n    }\n}\n\n#[expect(dead_code)]\n/// Returns a [`Vec`] containing all partitions.\npub(crate) fn partitions() -> anyhow::Result<Vec<Partition>> {\n    const PROC_MOUNTS: &str = \"/proc/mounts\";\n\n    let mut results = vec![];\n    let mut reader = BufReader::new(File::open(PROC_MOUNTS)?);\n    let mut line = String::new();\n\n    // This saves us from doing a string allocation on each iteration compared to\n    // `lines()`.\n    while let Ok(bytes) = reader.read_line(&mut line) {\n        if bytes > 0 {\n            if let Ok(partition) = Partition::from_str(&line) {\n                results.push(partition);\n            }\n\n            line.clear();\n        } else {\n            break;\n        }\n    }\n\n    Ok(results)\n}\n\n/// Returns a [`Vec`] containing all *physical* partitions. This is defined by\n/// [`FileSystem::is_physical()`].\npub(crate) fn physical_partitions() -> anyhow::Result<Vec<Partition>> {\n    const PROC_MOUNTS: &str = \"/proc/mounts\";\n\n    let mut results = vec![];\n    let mut reader = BufReader::new(File::open(PROC_MOUNTS)?);\n    let mut line = String::new();\n\n    // This saves us from doing a string allocation on each iteration compared to\n    // `lines()`.\n    while let Ok(bytes) = reader.read_line(&mut line) {\n        if bytes > 0 {\n            if let Ok(partition) = Partition::from_str(&line) {\n                if partition.fs_type().is_physical() {\n                    results.push(partition);\n                }\n            }\n\n            line.clear();\n        } else {\n            break;\n        }\n    }\n\n    Ok(results)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_fix_mount_point() {\n        let line = \"/run/media/test/Samsung\\\\040980\";\n\n        assert_eq!(fix_mount_point(line), \"/run/media/test/Samsung 980\");\n    }\n}\n"
  },
  {
    "path": "src/collection/disks/unix/macos/counters.rs",
    "content": "//! Based on [heim's implementation](https://github.com/heim-rs/heim/blob/master/heim-disk/src/sys/macos/counters.rs).\n\nuse super::io_kit::{self, get_dict, get_disks, get_i64, get_string};\nuse crate::collection::disks::IoCounters;\n\nfn get_device_io(device: io_kit::IoObject) -> anyhow::Result<IoCounters> {\n    let parent = device.service_parent()?;\n\n    // XXX: Re: Conform check being disabled.\n    //\n    // Okay, so this is weird.\n    //\n    // The problem is that if I have this check - this is what sources like psutil\n    // use, for example (see https://github.com/giampaolo/psutil/blob/7eadee31db2f038763a3a6f978db1ea76bbc4674/psutil/_psutil_osx.c#LL1422C20-L1422C20)\n    // then this will only return stuff like disk0.\n    //\n    // The problem with this is that there is *never* a disk0 *disk* entry to\n    // correspond to this, so there will be entries like disk1 or whatnot.\n    // Someone's done some digging on the gopsutil repo (https://github.com/shirou/gopsutil/issues/855#issuecomment-610016435), and it seems\n    // like this is a consequence of how Apple does logical volumes.\n    //\n    // So with all that said, what I've found is that I *can* still get a mapping -\n    // but I have to disable the conform check, which... is weird. I'm not sure\n    // if this is valid at all. But it *does* seem to match Activity Monitor\n    // with regards to disk activity, so... I guess we can leave this for\n    // now...?\n\n    // if !parent.conforms_to_block_storage_driver() {\n    //     anyhow::bail!(\"{parent:?}, the parent of {device:?} does not conform to\n    // IOBlockStorageDriver\") }\n\n    let disk_props = device.properties()?;\n    let parent_props = parent.properties()?;\n\n    let name = get_string(&disk_props, \"BSD Name\")?;\n    let stats = get_dict(&parent_props, \"Statistics\")?;\n\n    let read_bytes = get_i64(&stats, \"Bytes (Read)\")? as u64;\n    let write_bytes = get_i64(&stats, \"Bytes (Write)\")? as u64;\n\n    // let read_count = stats.get_i64(\"Operations (Read)\")? as u64;\n    // let write_count = stats.get_i64(\"Operations (Write)\")? as u64;\n\n    Ok(IoCounters::new(name, read_bytes, write_bytes))\n}\n\n/// Returns an iterator of disk I/O stats. Pulls data through IOKit.\npub fn io_stats() -> anyhow::Result<Vec<IoCounters>> {\n    Ok(get_disks()?.filter_map(|d| get_device_io(d).ok()).collect())\n}\n"
  },
  {
    "path": "src/collection/disks/unix/macos/io_kit/bindings.rs",
    "content": "//! C FFI bindings for [IOKit](https://developer.apple.com/documentation/iokit/).\n//!\n//! Based on [heim](https://github.com/heim-rs/heim/blob/master/heim-common/src/sys/macos/iokit/io_master_port.rs)\n//! and [sysinfo's implementation](https://github.com/GuillaumeGomez/sysinfo/blob/master/src/apple/macos/ffi.rs).\n//!\n//! Ideally, we can remove this if sysinfo ever gains disk I/O capabilities.\n\nuse core_foundation::{\n    base::{CFAllocatorRef, mach_port_t},\n    dictionary::CFMutableDictionaryRef,\n};\nuse libc::c_char;\nuse mach2::{kern_return::kern_return_t, port::MACH_PORT_NULL};\n\n#[expect(non_camel_case_types)]\npub type io_object_t = mach_port_t;\n\n#[expect(non_camel_case_types)]\npub type io_iterator_t = io_object_t;\n#[expect(non_camel_case_types)]\npub type io_registry_entry_t = io_object_t;\n\npub type IOOptionBits = u32;\n\n/// See https://github.com/1kc/librazermacos/pull/27#issuecomment-1042368531.\n#[expect(non_upper_case_globals)]\npub const kIOMasterPortDefault: mach_port_t = MACH_PORT_NULL;\n\n#[expect(non_upper_case_globals)]\npub const kIOServicePlane: &str = \"IOService\\0\";\n\n#[expect(non_upper_case_globals)]\npub const kIOMediaClass: &str = \"IOMedia\\0\";\n\n// SAFETY: Bindings like this are inherently unsafe. See [here](https://developer.apple.com/documentation/iokit) for\n// more details.\nunsafe extern \"C\" {\n\n    pub fn IOServiceGetMatchingServices(\n        mainPort: mach_port_t, matching: CFMutableDictionaryRef, existing: *mut io_iterator_t,\n    ) -> kern_return_t;\n\n    pub fn IOServiceMatching(name: *const c_char) -> CFMutableDictionaryRef;\n\n    pub fn IOIteratorNext(iterator: io_iterator_t) -> io_object_t;\n\n    pub fn IOObjectRelease(obj: io_object_t) -> kern_return_t;\n\n    pub fn IORegistryEntryGetParentEntry(\n        entry: io_registry_entry_t, plane: *const libc::c_char, parent: *mut io_registry_entry_t,\n    ) -> kern_return_t;\n\n    // pub fn IOObjectConformsTo(object: io_object_t, className: *const\n    // libc::c_char) -> mach2::boolean::boolean_t;\n\n    pub fn IORegistryEntryCreateCFProperties(\n        entry: io_registry_entry_t, properties: *mut CFMutableDictionaryRef,\n        allocator: CFAllocatorRef, options: IOOptionBits,\n    ) -> kern_return_t;\n\n}\n"
  },
  {
    "path": "src/collection/disks/unix/macos/io_kit/io_disks.rs",
    "content": "use anyhow::bail;\nuse mach2::kern_return;\n\nuse super::{IoIterator, bindings::*};\n\npub fn get_disks() -> anyhow::Result<IoIterator> {\n    let mut media_iter: io_iterator_t = 0;\n\n    // SAFETY: This is a safe syscall via IOKit, all the arguments should be safe.\n    let result = unsafe {\n        IOServiceGetMatchingServices(\n            kIOMasterPortDefault,\n            IOServiceMatching(kIOMediaClass.as_ptr().cast()),\n            &mut media_iter,\n        )\n    };\n\n    if result == kern_return::KERN_SUCCESS {\n        Ok(media_iter.into())\n    } else {\n        bail!(\"IOServiceGetMatchingServices failed, error code {result}\");\n    }\n}\n"
  },
  {
    "path": "src/collection/disks/unix/macos/io_kit/io_iterator.rs",
    "content": "//! Based on [heim's](https://github.com/heim-rs/heim/blob/master/heim-common/src/sys/macos/iokit/io_iterator.rs).\n//! implementation.\n\nuse std::ops::{Deref, DerefMut};\n\nuse mach2::kern_return;\n\nuse super::{bindings::*, io_object::IoObject};\n\n/// Safe wrapper around the IOKit `io_iterator_t` type.\n#[derive(Debug)]\npub struct IoIterator(io_iterator_t);\n\nimpl From<io_iterator_t> for IoIterator {\n    fn from(iter: io_iterator_t) -> IoIterator {\n        IoIterator(iter)\n    }\n}\n\nimpl Deref for IoIterator {\n    type Target = io_iterator_t;\n\n    fn deref(&self) -> &Self::Target {\n        &self.0\n    }\n}\n\nimpl DerefMut for IoIterator {\n    fn deref_mut(&mut self) -> &mut Self::Target {\n        &mut self.0\n    }\n}\n\nimpl Iterator for IoIterator {\n    type Item = IoObject;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        // Basically, we just stop when we hit 0.\n\n        // SAFETY: IOKit call, the passed argument (an `io_iterator_t`) is what is\n        // expected.\n        match unsafe { IOIteratorNext(self.0) } {\n            0 => None,\n            io_object => Some(IoObject::from(io_object)),\n        }\n    }\n}\n\nimpl Drop for IoIterator {\n    fn drop(&mut self) {\n        // SAFETY: IOKit call, the passed argument (an `io_iterator_t`) is what is\n        // expected.\n        let result = unsafe { IOObjectRelease(self.0) };\n        assert_eq!(result, kern_return::KERN_SUCCESS);\n    }\n}\n"
  },
  {
    "path": "src/collection/disks/unix/macos/io_kit/io_object.rs",
    "content": "//! Based on [heim's](https://github.com/heim-rs/heim/blob/master/heim-common/src/sys/macos/iokit/io_object.rs)\n//! implementation.\n\nuse std::mem;\n\nuse anyhow::{anyhow, bail};\nuse core_foundation::{\n    base::{CFType, TCFType, ToVoid, kCFAllocatorDefault},\n    dictionary::{\n        CFDictionary, CFDictionaryGetTypeID, CFDictionaryRef, CFMutableDictionary,\n        CFMutableDictionaryRef,\n    },\n    number::{CFNumber, CFNumberGetTypeID},\n    string::{CFString, CFStringGetTypeID},\n};\nuse mach2::kern_return;\n\nuse super::bindings::*;\n\n/// Safe wrapper around the IOKit `io_object_t` type.\n#[derive(Debug)]\npub struct IoObject(io_object_t);\n\nimpl IoObject {\n    /// Returns a typed dictionary with this object's properties.\n    pub fn properties(&self) -> anyhow::Result<CFDictionary<CFString, CFType>> {\n        // SAFETY: The IOKit call should be fine, the arguments are safe. The\n        // `assume_init` should also be fine, as we guard against it with a\n        // check against `result` to ensure it succeeded.\n        unsafe {\n            let mut props = mem::MaybeUninit::<CFMutableDictionaryRef>::uninit();\n\n            let result = IORegistryEntryCreateCFProperties(\n                self.0,\n                props.as_mut_ptr(),\n                kCFAllocatorDefault,\n                0,\n            );\n\n            if result != kern_return::KERN_SUCCESS {\n                bail!(\"IORegistryEntryCreateCFProperties failed, error code {result}.\")\n            } else {\n                let props = props.assume_init();\n                Ok(CFMutableDictionary::wrap_under_create_rule(props).to_immutable())\n            }\n        }\n    }\n\n    /// Gets the [`kIOServicePlane`] parent [`io_object_t`] for this\n    /// [`io_object_t`], if there is one.\n    pub fn service_parent(&self) -> anyhow::Result<IoObject> {\n        let mut parent: io_registry_entry_t = 0;\n\n        // SAFETY: IOKit call, the arguments should be safe.\n        let result = unsafe {\n            IORegistryEntryGetParentEntry(self.0, kIOServicePlane.as_ptr().cast(), &mut parent)\n        };\n\n        if result != kern_return::KERN_SUCCESS {\n            bail!(\"IORegistryEntryGetParentEntry failed, error code {result}.\")\n        } else {\n            Ok(parent.into())\n        }\n    }\n\n    // pub fn conforms_to_block_storage_driver(&self) -> bool {\n    //     // SAFETY: IOKit call, the arguments should be safe.\n    //     let result =\n    //         unsafe { IOObjectConformsTo(self.0,\n    // \"IOBlockStorageDriver\\0\".as_ptr().cast()) };\n\n    //     result != 0\n    // }\n}\n\nimpl From<io_object_t> for IoObject {\n    fn from(obj: io_object_t) -> IoObject {\n        IoObject(obj)\n    }\n}\n\nimpl Drop for IoObject {\n    fn drop(&mut self) {\n        // SAFETY: IOKit call, the argument here (an `io_object_t`) should be safe and\n        // expected.\n        let result = unsafe { IOObjectRelease(self.0) };\n        assert_eq!(result, kern_return::KERN_SUCCESS);\n    }\n}\n\npub fn get_dict(\n    dict: &CFDictionary<CFString, CFType>, raw_key: &'static str,\n) -> anyhow::Result<CFDictionary<CFString, CFType>> {\n    let key = CFString::from_static_string(raw_key);\n\n    dict.find(&key)\n        .map(|value_ref| {\n            // SAFETY: Only used for debug asserts, system API call that should be safe.\n            unsafe {\n                debug_assert!(value_ref.type_of() == CFDictionaryGetTypeID());\n            }\n\n            // \"Casting\" `CFDictionary<*const void, *const void>` into a needed dict type\n            let ptr = value_ref.to_void() as CFDictionaryRef;\n\n            // SAFETY: System API call, it should be safe?\n            unsafe { CFDictionary::wrap_under_get_rule(ptr) }\n        })\n        .ok_or_else(|| anyhow!(\"missing key\"))\n}\n\npub fn get_i64(\n    dict: &CFDictionary<CFString, CFType>, raw_key: &'static str,\n) -> anyhow::Result<i64> {\n    let key = CFString::from_static_string(raw_key);\n\n    dict.find(&key)\n        .and_then(|value_ref| {\n            // SAFETY: Only used for debug asserts, system API call that should be safe.\n            unsafe {\n                debug_assert!(value_ref.type_of() == CFNumberGetTypeID());\n            }\n            value_ref.downcast::<CFNumber>()\n        })\n        .and_then(|number| number.to_i64())\n        .ok_or_else(|| anyhow!(\"missing key\"))\n}\n\npub fn get_string(\n    dict: &CFDictionary<CFString, CFType>, raw_key: &'static str,\n) -> anyhow::Result<String> {\n    let key = CFString::from_static_string(raw_key);\n\n    dict.find(&key)\n        .and_then(|value_ref| {\n            // SAFETY: Only used for debug asserts, system API call that should be safe.\n            unsafe {\n                debug_assert!(value_ref.type_of() == CFStringGetTypeID());\n            }\n\n            value_ref.downcast::<CFString>()\n        })\n        .map(|cf_string| cf_string.to_string())\n        .ok_or_else(|| anyhow!(\"missing key\"))\n}\n"
  },
  {
    "path": "src/collection/disks/unix/macos/io_kit.rs",
    "content": "mod bindings;\nmod io_disks;\nmod io_iterator;\nmod io_object;\n\npub use io_disks::get_disks;\npub use io_iterator::*;\npub use io_object::*;\n"
  },
  {
    "path": "src/collection/disks/unix/macos/mod.rs",
    "content": "mod counters;\nmod io_kit;\n\npub use counters::*;\n"
  },
  {
    "path": "src/collection/disks/unix/other/bindings.rs",
    "content": "//! Based on [heim's](https://github.com/heim-rs/heim/blob/master/heim-disk/src/sys/unix/bindings/mod.rs)\n//! implementation.\n\nuse std::io::Error;\n\nconst MNT_NOWAIT: libc::c_int = 2;\n\n// SAFETY: Bindings like this are inherently unsafe.\nunsafe extern \"C\" {\n    fn getfsstat64(buf: *mut libc::statfs, bufsize: libc::c_int, flags: libc::c_int)\n    -> libc::c_int;\n}\n\n/// Returns all the mounts on the system at the moment.\npub(crate) fn mounts() -> anyhow::Result<Vec<libc::statfs>> {\n    // SAFETY: System API FFI call, arguments should be correct.\n    let expected_len = unsafe { getfsstat64(std::ptr::null_mut(), 0, MNT_NOWAIT) };\n\n    let mut mounts: Vec<libc::statfs> = Vec::with_capacity(expected_len as usize);\n\n    // SAFETY: System API FFI call, arguments should be correct.\n    let result = unsafe {\n        getfsstat64(\n            mounts.as_mut_ptr(),\n            std::mem::size_of::<libc::statfs>() as libc::c_int * expected_len,\n            MNT_NOWAIT,\n        )\n    };\n\n    if result == -1 {\n        Err(anyhow::Error::from(Error::last_os_error()).context(\"getfsstat64\"))\n    } else {\n        debug_assert_eq!(\n            expected_len, result,\n            \"Expected {expected_len} statfs entries, but instead got {result} entries\",\n        );\n\n        // SAFETY: We have a debug assert check, and if `result` is not correct (-1), we\n        // check against it. Otherwise, getfsstat64 should return the number of\n        // statfs structures if it succeeded.\n        //\n        // Source: https://man.freebsd.org/cgi/man.cgi?query=getfsstat&sektion=2&format=html\n        unsafe {\n            mounts.set_len(result as usize);\n        }\n        Ok(mounts)\n    }\n}\n"
  },
  {
    "path": "src/collection/disks/unix/other/mod.rs",
    "content": "mod bindings;\nmod partition;\n\npub(crate) use partition::*;\n"
  },
  {
    "path": "src/collection/disks/unix/other/partition.rs",
    "content": "use std::{\n    ffi::{CStr, CString},\n    os::unix::prelude::OsStrExt,\n    path::{Path, PathBuf},\n    str::FromStr,\n};\n\nuse anyhow::bail;\n\nuse super::bindings;\nuse crate::collection::disks::unix::{FileSystem, Usage};\n\npub(crate) struct Partition {\n    device: String,\n    mount_point: PathBuf,\n    fs_type: FileSystem,\n}\n\nimpl Partition {\n    /// Returns the mount point for this partition.\n    #[inline]\n    pub fn mount_point(&self) -> &Path {\n        self.mount_point.as_path()\n    }\n\n    /// Returns the [`FileSystem`] of this partition.\n    #[inline]\n    pub fn fs_type(&self) -> &FileSystem {\n        &self.fs_type\n    }\n\n    /// Returns the usage stats for this partition.\n    pub fn usage(&self) -> anyhow::Result<Usage> {\n        let path = CString::new(self.mount_point().as_os_str().as_bytes())?;\n        let mut vfs = std::mem::MaybeUninit::<libc::statvfs>::uninit();\n\n        // SAFETY: System API call. Arguments should be correct.\n        let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) };\n\n        if result == 0 {\n            // SAFETY: We check that it succeeded (result is 0), which means vfs should be\n            // populated.\n            Ok(Usage::new(unsafe { vfs.assume_init() }))\n        } else {\n            bail!(\"statvfs failed to get the disk usage for disk {path:?}\")\n        }\n    }\n\n    /// Returns the device name.\n    #[inline]\n    pub fn get_device_name(&self) -> String {\n        self.device.clone()\n    }\n}\n\nfn partitions_iter() -> anyhow::Result<impl Iterator<Item = Partition>> {\n    let mounts = bindings::mounts()?;\n\n    unsafe fn ptr_to_cow<'a>(ptr: *const i8) -> std::borrow::Cow<'a, str> {\n        unsafe { CStr::from_ptr(ptr).to_string_lossy() }\n    }\n\n    Ok(mounts.into_iter().map(|stat| {\n        // SAFETY: Should be a non-null pointer.\n        let device = unsafe { ptr_to_cow(stat.f_mntfromname.as_ptr()).to_string() };\n\n        let fs_type = {\n            // SAFETY: Should be a non-null pointer.\n            let fs_type_str = unsafe { ptr_to_cow(stat.f_fstypename.as_ptr()) };\n            FileSystem::from_str(&fs_type_str).unwrap_or(FileSystem::Other(fs_type_str.to_string()))\n        };\n\n        let mount_point = {\n            // SAFETY: Should be a non-null pointer.\n            let path_str = unsafe { ptr_to_cow(stat.f_mntonname.as_ptr()).to_string() };\n            PathBuf::from(path_str)\n        };\n\n        Partition {\n            device,\n            mount_point,\n            fs_type,\n        }\n    }))\n}\n\n#[expect(dead_code)]\n/// Returns a [`Vec`] containing all partitions.\npub(crate) fn partitions() -> anyhow::Result<Vec<Partition>> {\n    partitions_iter().map(|iter| iter.collect())\n}\n\n/// Returns a [`Vec`] containing all *physical* partitions. This is defined by\n/// [`FileSystem::is_physical()`].\npub(crate) fn physical_partitions() -> anyhow::Result<Vec<Partition>> {\n    partitions_iter().map(|iter| {\n        iter.filter(|partition| partition.fs_type().is_physical())\n            .collect()\n    })\n}\n"
  },
  {
    "path": "src/collection/disks/unix/usage.rs",
    "content": "pub struct Usage(libc::statvfs);\n\n// Note that x86 returns `u32` values while x86-64 returns `u64`s, so we convert\n// everything to `u64` for consistency.\n#[allow(clippy::useless_conversion)]\nimpl Usage {\n    pub(crate) fn new(vfs: libc::statvfs) -> Self {\n        Self(vfs)\n    }\n\n    /// Returns the total number of bytes available.\n    pub fn total(&self) -> u64 {\n        u64::from(self.0.f_blocks) * u64::from(self.0.f_frsize)\n    }\n\n    /// Returns the available number of bytes used. Note this is not necessarily\n    /// the same as [`Usage::free`].\n    pub fn available(&self) -> u64 {\n        u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize)\n    }\n\n    #[expect(dead_code)]\n    /// Returns the total number of bytes used. Equal to `total - available` on\n    /// Unix.\n    pub fn used(&self) -> u64 {\n        let avail_to_root = u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize);\n        self.total() - avail_to_root\n    }\n\n    /// Returns the total number of bytes free. Note this is not necessarily the\n    /// same as [`Usage::available`].\n    pub fn free(&self) -> u64 {\n        u64::from(self.0.f_bavail) * u64::from(self.0.f_frsize)\n    }\n}\n"
  },
  {
    "path": "src/collection/disks/unix.rs",
    "content": "//! Disk stats for Unix-like systems that aren't supported through other means.\n//! Officially, for now, this means Linux and macOS.\n\nmod file_systems;\n\nmod usage;\n\ncfg_if::cfg_if! {\n    if #[cfg(target_os = \"linux\")] {\n        mod linux;\n        pub use linux::*;\n    } else if #[cfg(target_os = \"macos\")] {\n        mod other;\n        use other::*;\n\n        mod macos;\n        pub use macos::*;\n    } else {\n        mod other;\n        use other::*;\n    }\n}\n\nuse file_systems::*;\nuse usage::*;\n\nuse super::{DiskHarvest, keep_disk_entry};\nuse crate::collection::DataCollector;\n\n/// Returns the disk usage of the mounted (and for now, physical) disks.\npub fn get_disk_usage(collector: &DataCollector) -> anyhow::Result<Vec<DiskHarvest>> {\n    let disk_filter = &collector.filters.disk_filter;\n    let mount_filter = &collector.filters.mount_filter;\n    let mut vec_disks: Vec<DiskHarvest> = Vec::new();\n\n    for partition in physical_partitions()? {\n        let name = partition.get_device_name();\n        let mount_point = partition.mount_point().to_string_lossy().to_string();\n\n        // Precedence ordering in the case where name and mount filters disagree,\n        // \"allow\" takes precedence over \"deny\".\n        //\n        // For implementation, we do this as follows:\n        // 1. Is the entry allowed through any filter? That is, does it match an entry\n        //    in a filter where `is_list_ignored` is `false`? If so, we always keep this\n        //    entry.\n        // 2. Is the entry denied through any filter? That is, does it match an entry in\n        //    a filter where `is_list_ignored` is `true`? If so, we always deny this\n        //    entry.\n        // 3. Anything else is allowed.\n\n        if keep_disk_entry(&name, &mount_point, disk_filter, mount_filter) {\n            // The usage line can fail in some cases (for example, if you use Void Linux +\n            // LUKS, see https://github.com/ClementTsang/bottom/issues/419 for details).\n            if let Ok(usage) = partition.usage() {\n                let total = usage.total();\n\n                vec_disks.push(DiskHarvest {\n                    free_space: Some(usage.free()),\n                    used_space: Some(total - usage.available()),\n                    total_space: Some(total),\n                    mount_point,\n                    name,\n                });\n            } else {\n                vec_disks.push(DiskHarvest {\n                    free_space: None,\n                    used_space: None,\n                    total_space: None,\n                    mount_point,\n                    name,\n                });\n            }\n        }\n    }\n\n    Ok(vec_disks)\n}\n"
  },
  {
    "path": "src/collection/disks/windows/bindings.rs",
    "content": "//! Windows bindings to get disk I/O counters.\n\nuse std::{\n    ffi::OsString,\n    io, mem,\n    os::windows::prelude::{OsStrExt, OsStringExt},\n    path::{Path, PathBuf},\n};\n\nuse anyhow::bail;\nuse windows::Win32::{\n    Foundation::{self, CloseHandle, HANDLE},\n    Storage::FileSystem::{\n        CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_READ, FILE_SHARE_WRITE,\n        FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, GetVolumeNameForVolumeMountPointW,\n        OPEN_EXISTING,\n    },\n    System::{\n        IO::DeviceIoControl,\n        Ioctl::{DISK_PERFORMANCE, IOCTL_DISK_PERFORMANCE},\n    },\n};\n\n/// Returns the I/O for a given volume.\n///\n/// Based on [psutil's implementation](https://github.com/giampaolo/psutil/blob/52fe5517f716dedf9c9918e56325e49a49146130/psutil/arch/windows/disk.c#L78-L83)\n/// and [heim's implementation](https://github.com/heim-rs/heim/blob/master/heim-disk/src/sys/windows/bindings/perf.rs).\nfn volume_io(volume: &Path) -> anyhow::Result<DISK_PERFORMANCE> {\n    if volume.is_file() {\n        // We assume the volume is a directory, so bail ASAP if it isn't.\n        bail!(\"Expects a directory to be passed in.\");\n    }\n\n    let volume = {\n        let mut wide_path = volume.as_os_str().encode_wide().collect::<Vec<_>>();\n\n        // We replace the trailing backslash and replace it with a \\0.\n        wide_path.pop();\n        wide_path.push(0x0000);\n\n        wide_path\n    };\n\n    // SAFETY: API call, arguments should be correct. We must also check after the\n    // call to ensure it is valid.\n    let h_device = unsafe {\n        CreateFileW(\n            windows::core::PCWSTR(volume.as_ptr()),\n            0,\n            FILE_SHARE_READ | FILE_SHARE_WRITE,\n            None,\n            OPEN_EXISTING,\n            FILE_FLAGS_AND_ATTRIBUTES(0),\n            Some(Foundation::HANDLE::default()),\n        )?\n    };\n\n    if h_device.is_invalid() {\n        bail!(\"Invalid handle value: {:?}\", io::Error::last_os_error());\n    }\n\n    let mut disk_performance = DISK_PERFORMANCE::default();\n    let mut bytes_returned = 0;\n\n    // SAFETY: This should be safe, we'll manually check the results and the\n    // arguments should be valid.\n    let ret = unsafe {\n        DeviceIoControl(\n            h_device,\n            IOCTL_DISK_PERFORMANCE,\n            None,\n            0,\n            Some(&mut disk_performance as *mut _ as _),\n            mem::size_of::<DISK_PERFORMANCE>() as u32,\n            Some(&mut bytes_returned),\n            None,\n        )\n    };\n\n    // SAFETY: This should be safe, we will check the result as well.\n    let handle_result = unsafe { CloseHandle(h_device) };\n    if let Err(err) = handle_result {\n        bail!(\"Handle error: {err:?}\");\n    }\n\n    if let Err(err) = ret {\n        bail!(\"Device I/O error: {err:?}\");\n    } else {\n        Ok(disk_performance)\n    }\n}\n\nfn current_volume(buffer: &[u16]) -> PathBuf {\n    let first_null = buffer.iter().position(|byte| *byte == 0x00).unwrap_or(0);\n    let path_string = OsString::from_wide(&buffer[..first_null]);\n\n    PathBuf::from(path_string)\n}\n\nfn close_find_handle(handle: HANDLE) -> anyhow::Result<()> {\n    // Clean up the handle.\n    // SAFETY: This should be safe, we will check the result as well.\n    let res = unsafe { FindVolumeClose(handle) };\n    Ok(res?)\n}\n\n/// Returns the I/O for all volumes.\n///\n/// Based on [psutil's implementation](https://github.com/giampaolo/psutil/blob/52fe5517f716dedf9c9918e56325e49a49146130/psutil/arch/windows/disk.c#L78-L83)\n/// and [heim's implementation](https://github.com/heim-rs/heim/blob/master/heim-disk/src/sys/windows/bindings/perf.rs).\npub(crate) fn all_volume_io() -> anyhow::Result<Vec<anyhow::Result<(DISK_PERFORMANCE, String)>>> {\n    const ERROR_NO_MORE_FILES: i32 = Foundation::ERROR_NO_MORE_FILES.0 as i32;\n    let mut ret = vec![];\n    let mut buffer = [0_u16; Foundation::MAX_PATH as usize];\n\n    // Get the first volume and add the stats needed.\n    // SAFETY: We must verify the handle is correct. If no volume is found, it will\n    // be set to `INVALID_HANDLE_VALUE`.\n    let handle = unsafe { FindFirstVolumeW(&mut buffer) }?;\n    if handle.is_invalid() {\n        bail!(\"Invalid handle value: {:?}\", io::Error::last_os_error());\n    }\n\n    {\n        let volume = current_volume(&buffer);\n        ret.push(volume_io(&volume).map(|res| (res, volume.to_string_lossy().to_string())));\n    }\n\n    // Now iterate until there are no more volumes.\n    while unsafe { FindNextVolumeW(handle, &mut buffer) }.is_ok() {\n        let volume = current_volume(&buffer);\n        ret.push(volume_io(&volume).map(|res| (res, volume.to_string_lossy().to_string())));\n    }\n\n    let err = io::Error::last_os_error();\n    match err.raw_os_error() {\n        Some(ERROR_NO_MORE_FILES) => {\n            // Iteration completed successfully, continue on.\n        }\n        _ => {\n            // Some error occurred.\n            close_find_handle(handle)?;\n            bail!(\"Error while iterating over volumes: {err:?}\");\n        }\n    }\n\n    close_find_handle(handle)?;\n\n    Ok(ret)\n}\n\n/// Returns the volume name from a mount name if possible.\npub(crate) fn volume_name_from_mount(mount: &str) -> anyhow::Result<String> {\n    // According to winapi docs 50 is a reasonable length to accommodate the volume\n    // path https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw\n    const VOLUME_MAX_LEN: usize = 50;\n\n    let mount = {\n        let mount_path = Path::new(mount);\n        let mut wide_path = mount_path.as_os_str().encode_wide().collect::<Vec<_>>();\n\n        // Always push on a \\0 character, without this it will occasionally break.\n        wide_path.push(0x0000);\n\n        wide_path\n    };\n    let mut buffer = [0_u16; VOLUME_MAX_LEN];\n\n    // SAFETY: API call, we must check the result for validating safety.\n    let result = unsafe {\n        GetVolumeNameForVolumeMountPointW(windows::core::PCWSTR(mount.as_ptr()), &mut buffer)\n    };\n\n    if let Err(err) = result {\n        bail!(\"Could not get volume name for mount point: {err:?}\");\n    } else {\n        Ok(current_volume(&buffer).to_string_lossy().to_string())\n    }\n}\n"
  },
  {
    "path": "src/collection/disks/windows.rs",
    "content": "//! Disk stats via sysinfo.\n\nmod bindings;\n\nuse bindings::*;\nuse itertools::Itertools;\n\nuse super::{DiskHarvest, keep_disk_entry};\nuse crate::collection::{DataCollector, disks::IoCounters};\n\n/// Returns I/O stats.\npub(crate) fn io_stats() -> anyhow::Result<Vec<IoCounters>> {\n    let volume_io = all_volume_io()?;\n\n    Ok(volume_io\n        .into_iter()\n        .map_ok(|(performance, volume_name)| {\n            let name = volume_name;\n            let read_bytes = performance.BytesRead as u64;\n            let write_bytes = performance.BytesWritten as u64;\n\n            IoCounters::new(name, read_bytes, write_bytes)\n        })\n        .flatten()\n        .collect::<Vec<_>>())\n}\n\npub(crate) fn get_disk_usage(collector: &DataCollector) -> anyhow::Result<Vec<DiskHarvest>> {\n    let disks = &collector.sys.disks;\n    let disk_filter = &collector.filters.disk_filter;\n    let mount_filter = &collector.filters.mount_filter;\n\n    Ok(disks\n        .iter()\n        .filter_map(|disk| {\n            let name = {\n                let name = disk.name();\n\n                if name.is_empty() {\n                    \"No Name\".to_string()\n                } else {\n                    name.to_os_string()\n                        .into_string()\n                        .unwrap_or_else(|_| \"Name Unavailable\".to_string())\n                }\n            };\n\n            let mount_point = disk\n                .mount_point()\n                .as_os_str()\n                .to_os_string()\n                .into_string()\n                .unwrap_or_else(|_| \"Mount Unavailable\".to_string());\n\n            let volume_name = volume_name_from_mount(&mount_point).ok();\n\n            if keep_disk_entry(&name, &mount_point, disk_filter, mount_filter) {\n                let free_space = disk.available_space();\n                let total_space = disk.total_space();\n                let used_space = total_space - free_space;\n\n                Some(DiskHarvest {\n                    name,\n                    mount_point,\n                    volume_name,\n                    free_space: Some(free_space),\n                    used_space: Some(used_space),\n                    total_space: Some(total_space),\n                })\n            } else {\n                None\n            }\n        })\n        .collect())\n}\n"
  },
  {
    "path": "src/collection/disks/zfs_io_counters.rs",
    "content": "use crate::collection::disks::IoCounters;\n\n/// Returns zpool I/O stats. Pulls data from `sysctl\n/// kstat.zfs.{POOL}.dataset.{objset-*}`\n#[cfg(target_os = \"freebsd\")]\npub fn zfs_io_stats() -> anyhow::Result<Vec<IoCounters>> {\n    use sysctl::Sysctl;\n    let zfs_ctls: Vec<_> = sysctl::Ctl::new(\"kstat.zfs.\")?\n        .into_iter()\n        .filter_map(|e| {\n            e.ok().and_then(|ctl| {\n                let name = ctl.name();\n                if let Ok(name) = name {\n                    if name.contains(\"objset-\")\n                        && (name.contains(\"dataset_name\")\n                            || name.contains(\"nwritten\")\n                            || name.contains(\"nread\"))\n                    {\n                        Some(ctl)\n                    } else {\n                        None\n                    }\n                } else {\n                    None\n                }\n            })\n        })\n        .collect();\n\n    use itertools::Itertools;\n    let results: Vec<IoCounters> = zfs_ctls\n        .iter()\n        .chunks(3)\n        .into_iter()\n        .filter_map(|chunk| {\n            let mut nread = 0;\n            let mut nwrite = 0;\n            let mut ds_name = String::new();\n            for ctl in chunk {\n                if let Ok(name) = ctl.name() {\n                    if name.contains(\"dataset_name\") {\n                        ds_name = ctl.value_string().ok()?;\n                    } else if name.contains(\"nread\") {\n                        if let Ok(sysctl::CtlValue::U64(val)) = ctl.value() {\n                            nread = val;\n                        }\n                    } else if name.contains(\"nwritten\") {\n                        if let Ok(sysctl::CtlValue::U64(val)) = ctl.value() {\n                            nwrite = val;\n                        }\n                    }\n                }\n            }\n            Some(IoCounters::new(ds_name, nread, nwrite))\n        })\n        .collect();\n    Ok(results)\n}\n\n/// Returns zpool I/O stats. Pulls data from `/proc/spl/kstat/zfs/*/objset-*`.\n#[cfg(target_os = \"linux\")]\npub fn zfs_io_stats() -> anyhow::Result<Vec<IoCounters>> {\n    if let Ok(zpools) = std::fs::read_dir(\"/proc/spl/kstat/zfs\") {\n        let zpools_vec: Vec<std::path::PathBuf> = zpools\n            .filter_map(|e| {\n                e.ok().and_then(|d| {\n                    let p = d.path();\n                    if p.is_dir() { Some(p) } else { None }\n                })\n            })\n            .collect();\n        let results = zpools_vec\n            .iter()\n            .filter_map(|zpool| {\n                // go through each pool\n                if let Ok(datasets) = std::fs::read_dir(zpool) {\n                    let datasets_vec: Vec<std::path::PathBuf> =\n                        datasets // go through dataset\n                            .filter_map(|e| {\n                                e.ok().and_then(|d| {\n                                    let p = d.path();\n                                    if p.is_file() && p.to_str()?.contains(\"objset-\") {\n                                        Some(p)\n                                    } else {\n                                        None\n                                    }\n                                })\n                            })\n                            .collect();\n                    let io_counters: Vec<IoCounters> = datasets_vec\n                        .iter()\n                        .filter_map(|ds| {\n                            // get io-counter from each dataset\n                            if let Ok(contents) = std::fs::read_to_string(ds) {\n                                let mut read = 0;\n                                let mut write = 0;\n                                let mut name = \"\";\n                                contents.lines().for_each(|line| {\n                                    if let Some((label, value)) = line.split_once(' ') {\n                                        match label {\n                                            \"dataset_name\" => {\n                                                if let Some((_type, val)) =\n                                                    value.trim_start().rsplit_once(' ')\n                                                {\n                                                    name = val;\n                                                }\n                                            }\n                                            \"nwritten\" => {\n                                                if let Some((_type, val)) =\n                                                    value.trim_start().rsplit_once(' ')\n                                                {\n                                                    if let Ok(number) = val.parse::<u64>() {\n                                                        write = number;\n                                                    }\n                                                }\n                                            }\n                                            \"nread\" => {\n                                                if let Some((_type, val)) =\n                                                    value.trim_start().rsplit_once(' ')\n                                                {\n                                                    if let Ok(number) = val.parse::<u64>() {\n                                                        read = number;\n                                                    }\n                                                }\n                                            }\n                                            _ => {}\n                                        }\n                                    }\n                                });\n\n                                let counter = IoCounters::new(name.to_owned(), read, write);\n                                Some(counter)\n                            } else {\n                                None\n                            }\n                        })\n                        .collect();\n                    Some(io_counters)\n                } else {\n                    None\n                }\n            })\n            .flatten()\n            .collect(); // combine io-counters\n        Ok(results)\n    } else {\n        Err(anyhow::anyhow!(\"Unable to open zfs proc directory\"))\n    }\n}\n"
  },
  {
    "path": "src/collection/disks.rs",
    "content": "//! Data collection about disks (e.g. I/O, usage, space).\n\ncfg_if! {\n    if #[cfg(target_os = \"freebsd\")] {\n        mod freebsd;\n        #[cfg(feature = \"zfs\")]\n        mod io_counters;\n        #[cfg(feature = \"zfs\")]\n        mod zfs_io_counters;\n        #[cfg(feature = \"zfs\")]\n        pub use io_counters::IoCounters;\n        pub(crate) use self::freebsd::*;\n    } else if #[cfg(target_os = \"windows\")] {\n        mod windows;\n        pub(crate) use self::windows::*;\n    } else if #[cfg(target_os = \"linux\")] {\n        mod unix;\n        #[cfg(feature = \"zfs\")]\n        mod zfs_io_counters;\n        pub(crate) use self::unix::*;\n    } else if #[cfg(target_os = \"macos\")] {\n        mod unix;\n        pub(crate) use self::unix::*;\n    } else {\n        mod other;\n        pub(crate) use self::other::*;\n    }\n}\n\nuse cfg_if::cfg_if;\nuse rustc_hash::FxHashMap as HashMap;\n\nuse crate::app::filter::Filter;\n\n#[derive(Clone, Debug, Default)]\npub struct DiskHarvest {\n    pub name: String,\n    pub mount_point: String,\n\n    /// Windows also contains an additional volume name field.\n    #[cfg(target_os = \"windows\")]\n    pub volume_name: Option<String>,\n\n    // TODO: Maybe unify all these?\n    pub free_space: Option<u64>,\n    pub used_space: Option<u64>,\n    pub total_space: Option<u64>,\n}\n\n#[derive(Clone, Debug)]\npub struct IoData {\n    pub read_bytes: u64,\n    pub write_bytes: u64,\n}\n\npub type IoHarvest = HashMap<String, Option<IoData>>;\n\ncfg_if! {\n    if #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"windows\"))] {\n        mod io_counters;\n        pub use io_counters::IoCounters;\n\n        /// Returns the I/O usage of certain mount points.\n        pub fn get_io_usage() -> anyhow::Result<IoHarvest> {\n            let mut io_hash: HashMap<String, Option<IoData>> = HashMap::default();\n\n            // TODO: Maybe rewrite this to not do a result of vec of result...\n            for io in io_stats()?.into_iter() {\n                let mount_point = io.device_name().to_string_lossy();\n\n                io_hash.insert(\n                    mount_point.to_string(),\n                    Some(IoData {\n                        read_bytes: io.read_bytes(),\n                        write_bytes: io.write_bytes(),\n                    }),\n                );\n            }\n\n            Ok(io_hash)\n        }\n    } else if #[cfg(not(target_os = \"freebsd\"))] {\n        pub fn get_io_usage() -> anyhow::Result<IoHarvest> {\n            anyhow::bail!(\"Unsupported OS\");\n        }\n    }\n}\n\n/// Whether to keep the current disk entry given the filters, disk name, and\n/// disk mount. Precedence ordering in the case where name and mount filters\n/// disagree, \"allow\" takes precedence over \"deny\".\n///\n/// For implementation, we do this as follows:\n///\n/// 1. Is the entry allowed through any filter? That is, does it match an entry\n///    in a filter where `is_list_ignored` is `false`? If so, we always keep\n///    this entry.\n/// 2. Is the entry denied through any filter? That is, does it match an entry\n///    in a filter where `is_list_ignored` is `true`? If so, we always deny this\n///    entry.\n/// 3. Anything else is allowed.\npub fn keep_disk_entry(\n    disk_name: &str, mount_point: &str, disk_filter: &Option<Filter>, mount_filter: &Option<Filter>,\n) -> bool {\n    match (disk_filter, mount_filter) {\n        (Some(d), Some(m)) => match (d.ignore_matches(), m.ignore_matches()) {\n            (true, true) => !(d.has_match(disk_name) || m.has_match(mount_point)),\n            (true, false) => {\n                if m.has_match(mount_point) {\n                    true\n                } else {\n                    d.should_keep(disk_name)\n                }\n            }\n            (false, true) => {\n                if d.has_match(disk_name) {\n                    true\n                } else {\n                    m.should_keep(mount_point)\n                }\n            }\n            (false, false) => d.has_match(disk_name) || m.has_match(mount_point),\n        },\n        (Some(d), None) => d.should_keep(disk_name),\n        (None, Some(m)) => m.should_keep(mount_point),\n        (None, None) => true,\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use regex::Regex;\n\n    use super::keep_disk_entry;\n    use crate::app::filter::Filter;\n\n    fn run_filter(disk_filter: &Option<Filter>, mount_filter: &Option<Filter>) -> Vec<usize> {\n        let targets = [\n            (\"/dev/nvme0n1p1\", \"/boot\"),\n            (\"/dev/nvme0n1p2\", \"/\"),\n            (\"/dev/nvme0n1p3\", \"/home\"),\n            (\"/dev/sda1\", \"/mnt/test\"),\n            (\"/dev/sda2\", \"/mnt/boot\"),\n        ];\n\n        targets\n            .into_iter()\n            .enumerate()\n            .filter_map(|(itx, (name, mount))| {\n                if keep_disk_entry(name, mount, disk_filter, mount_filter) {\n                    Some(itx)\n                } else {\n                    None\n                }\n            })\n            .collect()\n    }\n\n    #[test]\n    fn test_keeping_disk_entry() {\n        let disk_ignore = Some(Filter::new(true, vec![Regex::new(\"nvme\").unwrap()]));\n        let disk_keep = Some(Filter::new(false, vec![Regex::new(\"nvme\").unwrap()]));\n        let mount_ignore = Some(Filter::new(true, vec![Regex::new(\"boot\").unwrap()]));\n        let mount_keep = Some(Filter::new(false, vec![Regex::new(\"boot\").unwrap()]));\n\n        assert_eq!(run_filter(&None, &None), vec![0, 1, 2, 3, 4]);\n\n        assert_eq!(run_filter(&disk_ignore, &None), vec![3, 4]);\n        assert_eq!(run_filter(&disk_keep, &None), vec![0, 1, 2]);\n\n        assert_eq!(run_filter(&None, &mount_ignore), vec![1, 2, 3]);\n        assert_eq!(run_filter(&None, &mount_keep), vec![0, 4]);\n\n        assert_eq!(run_filter(&disk_ignore, &mount_ignore), vec![3]);\n        assert_eq!(run_filter(&disk_keep, &mount_ignore), vec![0, 1, 2, 3]);\n\n        assert_eq!(run_filter(&disk_ignore, &mount_keep), vec![0, 3, 4]);\n        assert_eq!(run_filter(&disk_keep, &mount_keep), vec![0, 1, 2, 4]);\n    }\n}\n"
  },
  {
    "path": "src/collection/error.rs",
    "content": "use anyhow::anyhow;\n\n/// An error to do with data collection.\n#[derive(Debug)]\npub enum CollectionError {\n    /// A general error to propagate back up. A wrapper around [`anyhow::Error`].\n    General(anyhow::Error),\n\n    /// The collection is unsupported.\n    #[allow(\n        dead_code,\n        reason = \"this is not used if everything is supported for the platform\"\n    )]\n    Unsupported,\n}\n\nimpl std::fmt::Display for CollectionError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            CollectionError::General(err) => err.fmt(f),\n            CollectionError::Unsupported => {\n                write!(\n                    f,\n                    \"bottom does not support this type of data collection for this platform.\"\n                )\n            }\n        }\n    }\n}\n\nimpl std::error::Error for CollectionError {}\n\n/// A [`Result`] with the error type being a [`DataCollectionError`].\npub(crate) type CollectionResult<T> = Result<T, CollectionError>;\n\nimpl From<std::io::Error> for CollectionError {\n    fn from(err: std::io::Error) -> Self {\n        Self::General(err.into())\n    }\n}\n\nimpl From<&'static str> for CollectionError {\n    fn from(msg: &'static str) -> Self {\n        Self::General(anyhow!(msg))\n    }\n}\n"
  },
  {
    "path": "src/collection/linux/utils.rs",
    "content": "use std::{fs, path::Path};\n\n/// Whether the temperature should *actually* be read during enumeration.\n/// Will return false if the state is not D0/unknown, or if it does not support\n/// `device/power_state`.\n///\n/// `path` is a path to the device itself (e.g. `/sys/class/hwmon/hwmon1/device`).\n#[inline]\npub fn is_device_awake(device: &Path) -> bool {\n    // Whether the temperature should *actually* be read during enumeration.\n    // Set to false if the device is in ACPI D3cold.\n    // Documented at https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-power_state\n    let power_state = device.join(\"power_state\");\n    if power_state.exists() {\n        if let Ok(state) = fs::read_to_string(power_state) {\n            let state = state.trim();\n            // The zenpower3 kernel module (incorrectly?) reports \"unknown\", causing this\n            // check to fail and temperatures to appear as zero instead of\n            // having the file not exist.\n            //\n            // Their self-hosted git instance has disabled sign up, so this bug can't be\n            // reported either.\n            state == \"D0\" || state == \"unknown\"\n        } else {\n            true\n        }\n    } else {\n        true\n    }\n}\n"
  },
  {
    "path": "src/collection/memory/arc.rs",
    "content": "#[cfg(all(feature = \"zfs\", any(target_os = \"linux\", target_os = \"freebsd\")))]\nuse super::MemData;\n#[cfg(all(feature = \"zfs\", any(target_os = \"linux\", target_os = \"freebsd\")))]\n/// Return ARC usage.\npub(crate) fn get_arc_usage() -> Option<(MemData, u64)> {\n    use std::num::NonZeroU64;\n\n    let (mem_total, mem_used, mem_min) = {\n        cfg_if::cfg_if! {\n            if #[cfg(target_os = \"linux\")] {\n                // TODO: [OPT] is this efficient?\n                use std::fs::read_to_string;\n                if let Ok(arc_stats) = read_to_string(\"/proc/spl/kstat/zfs/arcstats\") {\n                    let mut mem_arc = 0;\n                    let mut mem_total = 0;\n                    let mut mem_min = 0;\n                    let mut zfs_keys_read: u8 = 0;\n                    const ZFS_KEYS_NEEDED: u8 = 3;\n\n                    for line in arc_stats.lines() {\n                        if let Some((label, value)) = line.split_once(' ') {\n                            let to_write = match label {\n                                \"size\" => &mut mem_arc,\n                                \"c_max\" => &mut mem_total,\n                                \"c_min\" => &mut mem_min,\n                                _ => {\n                                    continue;\n                                }\n                            };\n\n                            if let Some((_type, number)) = value.trim_start().rsplit_once(' ') {\n                                // Parse the value, remember it's in bytes!\n                                if let Ok(number) = number.parse::<u64>() {\n                                    *to_write = number;\n                                    // We only need a few keys, so we can bail early.\n                                    zfs_keys_read += 1;\n                                    if zfs_keys_read == ZFS_KEYS_NEEDED {\n                                        break;\n                                    }\n                                }\n                            }\n                        }\n                    }\n                    (mem_total, mem_arc, mem_min)\n                } else {\n                    (0, 0, 0)\n                }\n            } else if #[cfg(target_os = \"freebsd\")] {\n                use sysctl::Sysctl;\n                if let (Ok(mem_arc_value), Ok(mem_sys_value), Ok(mem_min_value)) = (\n                    sysctl::Ctl::new(\"kstat.zfs.misc.arcstats.size\"),\n                    sysctl::Ctl::new(\"kstat.zfs.misc.arcstats.c_max\"),\n                    sysctl::Ctl::new(\"kstat.zfs.misc.arcstats.c_min\"),\n                ) {\n                    if let (Ok(sysctl::CtlValue::U64(arc)), Ok(sysctl::CtlValue::U64(mem)), Ok(sysctl::CtlValue::U64(min))) =\n                    (mem_arc_value.value(), mem_sys_value.value(), mem_min_value.value())\n                    {\n                        (mem, arc, min)\n                    } else {\n                        (0, 0, 0)\n                    }\n                } else {\n                    (0, 0, 0)\n                }\n            } else {\n                (0, 0, 0)\n            }\n        }\n    };\n\n    NonZeroU64::new(mem_total).map(|total_bytes| {\n        (\n            MemData {\n                total_bytes,\n                used_bytes: mem_used,\n            },\n            mem_min,\n        )\n    })\n}\n"
  },
  {
    "path": "src/collection/memory/sysinfo.rs",
    "content": "//! Collecting memory data using sysinfo.\n\nuse std::num::NonZeroU64;\n\nuse sysinfo::System;\n\nuse crate::collection::memory::MemData;\n\n#[inline]\nfn get_usage(used: u64, total: u64) -> Option<MemData> {\n    NonZeroU64::new(total).map(|total_bytes| MemData {\n        total_bytes,\n        used_bytes: used,\n    })\n}\n\n/// Returns RAM usage.\npub(crate) fn get_ram_usage(sys: &System) -> Option<MemData> {\n    get_usage(sys.used_memory(), sys.total_memory())\n}\n\n/// Returns SWAP usage.\n#[cfg(not(target_os = \"windows\"))]\npub(crate) fn get_swap_usage(sys: &System) -> Option<MemData> {\n    get_usage(sys.used_swap(), sys.total_swap())\n}\n\n/// Returns cache usage. sysinfo has no way to do this directly but it should\n/// equal the difference between the available and free memory. Free memory is\n/// defined as memory not containing any data, which means cache and buffer\n/// memory are not \"free\". Available memory is defined as memory able\n/// to be allocated by processes, which includes cache and buffer memory. On\n/// Windows, this will always be 0. For more information, see [docs](https://docs.rs/sysinfo/latest/sysinfo/struct.System.html#method.available_memory)\n/// and [memory explanation](https://askubuntu.com/questions/867068/what-is-available-memory-while-using-free-command)\n#[cfg(not(target_os = \"windows\"))]\npub(crate) fn get_cache_usage(sys: &System) -> Option<MemData> {\n    let mem_used = sys.available_memory().saturating_sub(sys.free_memory());\n    let mem_total = sys.total_memory();\n\n    get_usage(mem_used, mem_total)\n}\n"
  },
  {
    "path": "src/collection/memory/windows.rs",
    "content": "use std::{mem::zeroed, num::NonZeroU64};\n\nuse sysinfo::System;\nuse windows::{\n    Win32::{\n        Foundation::ERROR_SUCCESS,\n        System::Performance::{\n            PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE, PDH_HCOUNTER, PDH_HQUERY, PdhAddEnglishCounterW,\n            PdhCloseQuery, PdhCollectQueryData, PdhGetFormattedCounterValue, PdhOpenQueryW,\n            PdhRemoveCounter,\n        },\n    },\n    core::w,\n};\n\nuse crate::collection::memory::MemData;\n\n/// Get swap memory usage on Windows. This does it by using checking Windows' performance counters.\n/// This is based on the technique done by psutil [here](https://github.com/giampaolo/psutil/pull/2160).\n///\n/// Also see:\n/// - <https://github.com/GuillaumeGomez/sysinfo/blob/master/src/windows/system.rs>\n/// - <https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-performance_information>\n/// - <https://en.wikipedia.org/wiki/Commit_charge>.\n/// - <https://github.com/giampaolo/psutil/issues/2431>\n/// - <https://github.com/oshi/oshi/issues/1175>\n/// - <https://github.com/oshi/oshi/issues/1182>\npub(crate) fn get_swap_usage(sys: &System) -> Option<MemData> {\n    let total_bytes = NonZeroU64::new(sys.total_swap())?;\n\n    // See https://kennykerr.ca/rust-getting-started/string-tutorial.html\n    let query = w!(\"\\\\Paging File(_Total)\\\\% Usage\");\n\n    // SAFETY: Hits a few Windows APIs; this should be safe as we check each step, and\n    // we clean up at the end.\n    unsafe {\n        let mut query_handle: PDH_HQUERY = zeroed();\n        let mut counter_handle: PDH_HCOUNTER = zeroed();\n        let mut counter_value: PDH_FMT_COUNTERVALUE = zeroed();\n\n        if PdhOpenQueryW(None, 0, &mut query_handle) != ERROR_SUCCESS.0 {\n            return None;\n        }\n\n        if PdhAddEnglishCounterW(query_handle, query, 0, &mut counter_handle) != ERROR_SUCCESS.0 {\n            return None;\n        }\n\n        // May fail if swap is disabled.\n        if PdhCollectQueryData(query_handle) != ERROR_SUCCESS.0 {\n            return None;\n        }\n\n        if PdhGetFormattedCounterValue(counter_handle, PDH_FMT_DOUBLE, None, &mut counter_value)\n            != ERROR_SUCCESS.0\n        {\n            // If we fail, still clean up.\n            PdhCloseQuery(query_handle);\n            return None;\n        }\n\n        let use_percentage = counter_value.Anonymous.doubleValue;\n\n        // Cleanup.\n        PdhRemoveCounter(counter_handle);\n        PdhCloseQuery(query_handle);\n\n        let used_bytes = (total_bytes.get() as f64 / 100.0 * use_percentage) as u64;\n        Some(MemData {\n            used_bytes,\n            total_bytes,\n        })\n    }\n}\n\n#[cfg(all(target_os = \"windows\", test))]\nmod tests {\n    use sysinfo::{MemoryRefreshKind, RefreshKind};\n\n    use super::*;\n\n    #[test]\n    fn test_windows_get_swap_usage() {\n        let sys = System::new_with_specifics(\n            RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_swap()),\n        );\n\n        let swap_usage = get_swap_usage(&sys);\n        if sys.total_swap() > 0 {\n            // Not sure if we can guarantee this to always pass on a machine, so I'll just print out.\n            println!(\"swap: {swap_usage:?}\");\n        } else {\n            println!(\"No swap, skipping.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/collection/memory.rs",
    "content": "//! Memory data collection.\n\nuse std::num::NonZeroU64;\n\npub(crate) use self::sysinfo::get_ram_usage;\n\npub mod sysinfo;\n\ncfg_if::cfg_if! {\n    if #[cfg(target_os = \"windows\")] {\n        mod windows;\n        pub(crate) use self::windows::get_swap_usage;\n    } else {\n        pub(crate) use self::sysinfo::{get_cache_usage, get_swap_usage};\n    }\n}\n\n#[cfg(feature = \"zfs\")]\npub mod arc;\n\n#[derive(Debug, Clone)]\npub struct MemData {\n    pub used_bytes: u64,\n    pub total_bytes: NonZeroU64,\n}\n\nimpl MemData {\n    /// Return the use percentage.\n    #[inline]\n    pub fn percentage(&self) -> f64 {\n        let used = self.used_bytes as f64;\n        let total = self.total_bytes.get() as f64;\n\n        used / total * 100.0\n    }\n}\n"
  },
  {
    "path": "src/collection/network/sysinfo.rs",
    "content": "//! Gets network data via sysinfo.\n\nuse std::time::Instant;\n\nuse sysinfo::Networks;\n\nuse super::NetworkHarvest;\nuse crate::app::filter::Filter;\n\n// TODO: Eventually make it so that this thing also takes individual usage into\n// account, so we can show per-interface!\npub fn get_network_data(\n    networks: &Networks, prev_net_access_time: Instant, prev_net_rx: &mut u64,\n    prev_net_tx: &mut u64, prev_net_rx_packets: &mut u64, prev_net_tx_packets: &mut u64,\n    curr_time: Instant, filter: &Option<Filter>,\n) -> NetworkHarvest {\n    let mut total_rx: u64 = 0;\n    let mut total_tx: u64 = 0;\n    let mut total_rx_packets: u64 = 0;\n    let mut total_tx_packets: u64 = 0;\n\n    for (name, network) in networks {\n        let to_keep = if let Some(filter) = filter {\n            filter.should_keep(name)\n        } else {\n            true\n        };\n\n        if to_keep {\n            total_rx += network.total_received() * 8;\n            total_tx += network.total_transmitted() * 8;\n            total_rx_packets += network.total_packets_received();\n            total_tx_packets += network.total_packets_transmitted();\n        }\n    }\n\n    let elapsed_time = curr_time.duration_since(prev_net_access_time).as_secs_f64();\n\n    let (rx, tx, rx_packets, tx_packets) = if elapsed_time == 0.0 {\n        (0, 0, 0, 0)\n    } else {\n        (\n            ((total_rx.saturating_sub(*prev_net_rx)) as f64 / elapsed_time) as u64,\n            ((total_tx.saturating_sub(*prev_net_tx)) as f64 / elapsed_time) as u64,\n            ((total_rx_packets.saturating_sub(*prev_net_rx_packets)) as f64 / elapsed_time) as u64,\n            ((total_tx_packets.saturating_sub(*prev_net_tx_packets)) as f64 / elapsed_time) as u64,\n        )\n    };\n\n    *prev_net_rx = total_rx;\n    *prev_net_tx = total_tx;\n    *prev_net_rx_packets = total_rx_packets;\n    *prev_net_tx_packets = total_tx_packets;\n    NetworkHarvest {\n        rx,\n        tx,\n        total_rx,\n        total_tx,\n        rx_packets,\n        tx_packets,\n        total_rx_packets,\n        total_tx_packets,\n    }\n}\n"
  },
  {
    "path": "src/collection/network.rs",
    "content": "//! Data collection for network usage/IO.\n\npub mod sysinfo;\npub use self::sysinfo::*;\n\n#[derive(Default, Clone, Debug)]\n/// All units in bits.\npub struct NetworkHarvest {\n    pub rx: u64,\n    pub tx: u64,\n    pub total_rx: u64,\n    pub total_tx: u64,\n    pub rx_packets: u64,\n    pub tx_packets: u64,\n    pub total_rx_packets: u64,\n    pub total_tx_packets: u64,\n}\n\nimpl NetworkHarvest {\n    pub fn first_run_cleanup(&mut self) {\n        self.rx = 0;\n        self.tx = 0;\n    }\n}\n"
  },
  {
    "path": "src/collection/nvidia.rs",
    "content": "use std::{num::NonZeroU64, sync::OnceLock};\n\nuse nohash::IntMap;\nuse nvml_wrapper::{\n    Nvml, enum_wrappers::device::TemperatureSensor, enums::device::UsedGpuMemory, error::NvmlError,\n};\n\nuse crate::{\n    app::{filter::Filter, layout_manager::UsedWidgets},\n    collection::{memory::MemData, processes::Pid, temperature::TempSensorData},\n};\n\npub static NVML_DATA: OnceLock<Result<Nvml, NvmlError>> = OnceLock::new();\n\npub struct GpusData {\n    pub memory: Option<Vec<(String, MemData)>>,\n    pub temperature: Option<Vec<TempSensorData>>,\n    pub procs: Option<(u64, Vec<IntMap<Pid, (u64, u32)>>)>,\n}\n\n/// Wrapper around Nvml::init\n///\n/// On Linux, if `Nvml::init()` fails, this function attempts to explicitly load\n/// the library from `libnvidia-ml.so.1`. On other platforms, it simply calls `Nvml::init`.\n///\n/// This is a workaround until https://github.com/Cldfire/nvml-wrapper/pull/63 is accepted.\n/// Then, we can go back to calling `Nvml::init` directly on all platforms.\nfn init_nvml() -> Result<Nvml, NvmlError> {\n    #[cfg(not(target_os = \"linux\"))]\n    {\n        Nvml::init()\n    }\n    #[cfg(target_os = \"linux\")]\n    {\n        match Nvml::init() {\n            Ok(nvml) => Ok(nvml),\n            Err(_) => Nvml::builder()\n                .lib_path(std::ffi::OsStr::new(\"libnvidia-ml.so.1\"))\n                .init(),\n        }\n    }\n}\n\n/// Returns the GPU data from NVIDIA cards.\n#[inline]\npub fn get_nvidia_vecs(\n    filter: &Option<Filter>, widgets_to_harvest: &UsedWidgets,\n) -> Option<GpusData> {\n    if let Ok(nvml) = NVML_DATA.get_or_init(init_nvml) {\n        if let Ok(num_gpu) = nvml.device_count() {\n            let mut temp_vec = Vec::with_capacity(num_gpu as usize);\n            let mut mem_vec = Vec::with_capacity(num_gpu as usize);\n            let mut proc_vec = Vec::with_capacity(num_gpu as usize);\n            let mut total_mem = 0;\n\n            for i in 0..num_gpu {\n                if let Ok(device) = nvml.device_by_index(i) {\n                    if let Ok(name) = device.name() {\n                        if widgets_to_harvest.use_mem {\n                            if let Ok(mem) = device.memory_info() {\n                                if let Some(total_bytes) = NonZeroU64::new(mem.total) {\n                                    mem_vec.push((\n                                        name.clone(),\n                                        MemData {\n                                            total_bytes,\n                                            used_bytes: mem.used,\n                                        },\n                                    ));\n                                }\n                            }\n                        }\n\n                        if widgets_to_harvest.use_temp\n                            && Filter::optional_should_keep(filter, &name)\n                        {\n                            if let Ok(temperature) = device.temperature(TemperatureSensor::Gpu) {\n                                temp_vec.push(TempSensorData {\n                                    name,\n                                    temperature: Some(temperature as f32),\n                                });\n                            } else {\n                                temp_vec.push(TempSensorData {\n                                    name,\n                                    temperature: None,\n                                });\n                            }\n                        }\n                    }\n\n                    if widgets_to_harvest.use_proc {\n                        let mut procs = IntMap::default();\n\n                        if let Ok(gpu_procs) = device.process_utilization_stats(None) {\n                            for proc in gpu_procs {\n                                let pid = proc.pid as Pid;\n                                let gpu_util = proc.sm_util + proc.enc_util + proc.dec_util;\n                                procs.insert(pid, (0, gpu_util));\n                            }\n                        }\n\n                        if let Ok(compute_procs) = device.running_compute_processes() {\n                            for proc in compute_procs {\n                                let pid = proc.pid as Pid;\n                                let gpu_mem = match proc.used_gpu_memory {\n                                    UsedGpuMemory::Used(val) => val,\n                                    UsedGpuMemory::Unavailable => 0,\n                                };\n                                if let Some(prev) = procs.get(&pid) {\n                                    procs.insert(pid, (gpu_mem, prev.1));\n                                } else {\n                                    procs.insert(pid, (gpu_mem, 0));\n                                }\n                            }\n                        }\n\n                        // Use the legacy API too but prefer newer API results\n                        if let Ok(graphics_procs) = device.running_graphics_processes_v2() {\n                            for proc in graphics_procs {\n                                let pid = proc.pid as Pid;\n                                let gpu_mem = match proc.used_gpu_memory {\n                                    UsedGpuMemory::Used(val) => val,\n                                    UsedGpuMemory::Unavailable => 0,\n                                };\n                                if let Some(prev) = procs.get(&pid) {\n                                    procs.insert(pid, (gpu_mem, prev.1));\n                                } else {\n                                    procs.insert(pid, (gpu_mem, 0));\n                                }\n                            }\n                        }\n\n                        if let Ok(graphics_procs) = device.running_graphics_processes() {\n                            for proc in graphics_procs {\n                                let pid = proc.pid as Pid;\n                                let gpu_mem = match proc.used_gpu_memory {\n                                    UsedGpuMemory::Used(val) => val,\n                                    UsedGpuMemory::Unavailable => 0,\n                                };\n                                if let Some(prev) = procs.get(&pid) {\n                                    procs.insert(pid, (gpu_mem, prev.1));\n                                } else {\n                                    procs.insert(pid, (gpu_mem, 0));\n                                }\n                            }\n                        }\n\n                        if !procs.is_empty() {\n                            proc_vec.push(procs);\n                        }\n\n                        // running total for proc %\n                        if let Ok(mem) = device.memory_info() {\n                            total_mem += mem.total;\n                        }\n                    }\n                }\n            }\n\n            Some(GpusData {\n                memory: if !mem_vec.is_empty() {\n                    Some(mem_vec)\n                } else {\n                    None\n                },\n                temperature: if !temp_vec.is_empty() {\n                    Some(temp_vec)\n                } else {\n                    None\n                },\n                procs: if !proc_vec.is_empty() {\n                    Some((total_mem, proc_vec))\n                } else {\n                    None\n                },\n            })\n        } else {\n            None\n        }\n    } else {\n        None\n    }\n}\n"
  },
  {
    "path": "src/collection/processes/freebsd.rs",
    "content": "//! Process data collection for FreeBSD.  Uses sysinfo.\n\nuse std::{io, process::Command};\n\nuse nohash::IntMap;\nuse serde::{Deserialize, Deserializer};\n\nuse crate::collection::{Pid, deserialize_xo, processes::UnixProcessExt};\n\n#[derive(Deserialize, Debug, Default)]\n#[serde(rename_all = \"kebab-case\")]\nstruct ProcessInformation {\n    process: Vec<ProcessRow>,\n}\n\n#[derive(Deserialize, Debug)]\n#[serde(rename_all = \"kebab-case\")]\nstruct ProcessRow {\n    #[serde(deserialize_with = \"pid\")]\n    pid: i32,\n    #[serde(deserialize_with = \"percent_cpu\")]\n    percent_cpu: f32,\n}\n\npub(crate) struct FreeBSDProcessExt;\n\nimpl UnixProcessExt for FreeBSDProcessExt {\n    #[inline]\n    fn has_backup_proc_cpu_fn() -> bool {\n        true\n    }\n\n    fn backup_proc_cpu(pids: &[Pid]) -> io::Result<IntMap<Pid, f32>> {\n        if pids.is_empty() {\n            return Ok(IntMap::default());\n        }\n\n        let output = Command::new(\"ps\")\n            .args([\"--libxo\", \"json\", \"-o\", \"pid,pcpu\", \"-p\"])\n            .args(pids.iter().map(i32::to_string))\n            .output()?;\n\n        deserialize_xo(\"process-information\", &output.stdout).map(\n            |process_info: ProcessInformation| {\n                process_info\n                    .process\n                    .into_iter()\n                    .map(|row| (row.pid, row.percent_cpu))\n                    .collect()\n            },\n        )\n    }\n}\n\nfn pid<'de, D>(deserializer: D) -> Result<i32, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    let s = String::deserialize(deserializer)?;\n    s.parse().map_err(serde::de::Error::custom)\n}\n\nfn percent_cpu<'de, D>(deserializer: D) -> Result<f32, D::Error>\nwhere\n    D: Deserializer<'de>,\n{\n    let s = String::deserialize(deserializer)?;\n    s.parse().map_err(serde::de::Error::custom)\n}\n"
  },
  {
    "path": "src/collection/processes/linux/mod.rs",
    "content": "//! Process data collection for Linux.\n\nmod process;\n\nuse std::{\n    fs::{self, File},\n    io::{BufRead, BufReader},\n    time::Duration,\n};\n\nuse concat_string::concat_string;\nuse itertools::Itertools;\nuse process::*;\nuse rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};\nuse sysinfo::ProcessStatus;\n\nuse super::{Pid, ProcessHarvest, UserTable, process_status_str};\nuse crate::collection::{DataCollector, error::CollectionResult, processes::ProcessType};\n\n/// Maximum character length of a `/proc/<PID>/stat` process name (the length is 16,\n/// but this includes a null terminator).\n///\n/// If it's equal or greater, then we instead refer to the command for the name.\nconst MAX_STAT_NAME_LEN: usize = 15;\n\n#[derive(Debug, Clone, Default)]\npub struct PrevProcDetails {\n    total_read_bytes: u64,\n    total_write_bytes: u64,\n    cpu_time: u64,\n}\n\n/// Given `/proc/stat` file contents, determine the idle and non-idle values of\n/// the CPU used to calculate CPU usage.\nfn fetch_cpu_usage(line: &str) -> (f64, f64) {\n    /// Converts a `Option<&str>` value to an f64. If it fails to parse or is\n    /// `None`, it will return `0_f64`.\n    fn str_to_f64(val: Option<&str>) -> f64 {\n        val.and_then(|v| v.parse::<f64>().ok()).unwrap_or(0_f64)\n    }\n\n    let mut val = line.split_whitespace();\n    let user = str_to_f64(val.next());\n    let nice: f64 = str_to_f64(val.next());\n    let system: f64 = str_to_f64(val.next());\n    let idle: f64 = str_to_f64(val.next());\n    let iowait: f64 = str_to_f64(val.next());\n    let irq: f64 = str_to_f64(val.next());\n    let softirq: f64 = str_to_f64(val.next());\n    let steal: f64 = str_to_f64(val.next());\n\n    // Note we do not get guest/guest_nice, as they are calculated as part of\n    // user/nice respectively See https://github.com/htop-dev/htop/blob/main/linux/LinuxProcessList.c\n    let idle = idle + iowait;\n    let non_idle = user + nice + system + irq + softirq + steal;\n\n    (idle, non_idle)\n}\n\nstruct CpuUsage {\n    /// Difference between the total delta and the idle delta.\n    cpu_usage: f64,\n\n    /// Overall CPU usage as a fraction.\n    cpu_fraction: f64,\n}\n\nfn cpu_usage_calculation(\n    prev_idle: &mut f64, prev_non_idle: &mut f64,\n) -> CollectionResult<CpuUsage> {\n    let (idle, non_idle) = {\n        // From SO answer: https://stackoverflow.com/a/23376195\n        let first_line = {\n            // We just need a single line from this file. Read it and return it.\n            let mut reader = BufReader::new(File::open(\"/proc/stat\")?);\n            let mut buffer = String::new();\n            reader.read_line(&mut buffer)?;\n\n            buffer\n        };\n\n        fetch_cpu_usage(&first_line)\n    };\n\n    let total = idle + non_idle;\n    let prev_total = *prev_idle + *prev_non_idle;\n\n    let total_delta = total - prev_total;\n    let idle_delta = idle - *prev_idle;\n\n    *prev_idle = idle;\n    *prev_non_idle = non_idle;\n\n    // TODO: Should these return errors instead?\n    let cpu_usage = if total_delta - idle_delta != 0.0 {\n        total_delta - idle_delta\n    } else {\n        1.0\n    };\n\n    let cpu_fraction = if total_delta != 0.0 {\n        cpu_usage / total_delta\n    } else {\n        0.0\n    };\n\n    Ok(CpuUsage {\n        cpu_usage,\n        cpu_fraction,\n    })\n}\n\n/// Returns the usage and a new set of process times.\n///\n/// NB: cpu_fraction should be represented WITHOUT the x100 factor!\nfn get_linux_cpu_usage(\n    stat: &Stat, cpu_usage: f64, cpu_fraction: f64, prev_proc_times: u64,\n    use_current_cpu_total: bool,\n) -> (f32, u64) {\n    // Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556\n    let new_proc_times = stat.utime + stat.stime;\n    let diff = (new_proc_times - prev_proc_times) as f64; // No try_from for u64 -> f64... oh well.\n\n    if cpu_usage == 0.0 {\n        (0.0, new_proc_times)\n    } else if use_current_cpu_total {\n        (((diff / cpu_usage) * 100.0) as f32, new_proc_times)\n    } else {\n        (\n            ((diff / cpu_usage) * 100.0 * cpu_fraction) as f32,\n            new_proc_times,\n        )\n    }\n}\n\nfn read_proc(\n    prev_proc: &PrevProcDetails, process: Process, args: ReadProcArgs, user_table: &mut UserTable,\n    thread_parent: Option<Pid>,\n) -> CollectionResult<(ProcessHarvest, u64)> {\n    let Process {\n        pid: _pid,\n        uid,\n        stat,\n        io,\n        cmdline,\n    } = process;\n\n    let ReadProcArgs {\n        use_current_cpu_total,\n        cpu_usage,\n        cpu_fraction,\n        total_memory,\n        time_difference_in_secs,\n        system_uptime,\n        get_process_threads: _,\n    } = args;\n\n    let process_state_char = stat.state;\n    let process_state = (\n        process_status_str(ProcessStatus::from(process_state_char)),\n        process_state_char,\n    );\n    let (cpu_usage_percent, new_process_times) = get_linux_cpu_usage(\n        &stat,\n        cpu_usage,\n        cpu_fraction,\n        prev_proc.cpu_time,\n        use_current_cpu_total,\n    );\n\n    let (parent_pid, process_type) = if let Some(thread_parent) = thread_parent {\n        (Some(thread_parent), ProcessType::ProcessThread)\n    } else if stat.is_kernel_thread {\n        (Some(stat.ppid), ProcessType::Kernel)\n    } else {\n        (Some(stat.ppid), ProcessType::Regular)\n    };\n\n    let mem_usage = stat.rss_bytes();\n    let mem_usage_percent = (mem_usage as f64 / total_memory as f64 * 100.0) as f32;\n    let virtual_mem = stat.vsize;\n\n    // XXX: This can fail if permission is denied.\n    let (total_read, total_write, read_per_sec, write_per_sec) = if let Some(io) = io {\n        let total_read = io.read_bytes;\n        let total_write = io.write_bytes;\n        let prev_total_read = prev_proc.total_read_bytes;\n        let prev_total_write = prev_proc.total_write_bytes;\n\n        let read_per_sec = total_read\n            .saturating_sub(prev_total_read)\n            .checked_div(time_difference_in_secs)\n            .unwrap_or(0);\n\n        let write_per_sec = total_write\n            .saturating_sub(prev_total_write)\n            .checked_div(time_difference_in_secs)\n            .unwrap_or(0);\n\n        (total_read, total_write, read_per_sec, write_per_sec)\n    } else {\n        (0, 0, 0, 0)\n    };\n\n    let user = uid.and_then(|uid| user_table.uid_to_username(uid).ok());\n\n    let time = if let Ok(ticks_per_sec) = u32::try_from(rustix::param::clock_ticks_per_second()) {\n        if ticks_per_sec == 0 {\n            Duration::ZERO\n        } else {\n            Duration::from_secs(\n                system_uptime.saturating_sub(stat.start_time / ticks_per_sec as u64),\n            )\n        }\n    } else {\n        Duration::ZERO\n    };\n\n    let (command, name) = {\n        let comm = stat.comm;\n        if let Some(cmdline) = cmdline {\n            if cmdline.is_empty() {\n                (concat_string!(\"[\", comm, \"]\"), comm)\n            } else {\n                // If the comm fits then we'll default to whatever is set.\n                // If it doesn't, we need to do some magic to determine what it's\n                // supposed to be.\n\n                // TODO: We might want to re-evaluate if we want to do it like this,\n                // as it turns out I was dumb and sometimes comm != process name...\n                //\n                // What we should do is store:\n                // - basename (what we're kinda doing now, except we're gating on comm length)\n                // - command (full thing)\n                // - comm (as a separate thing)\n                //\n                // Stuff like htop also offers the option to \"highlight\" basename and comm in command. Might be neat?\n                let name = if comm.len() >= MAX_STAT_NAME_LEN {\n                    binary_name_from_cmdline(&cmdline)\n                } else {\n                    comm\n                };\n\n                (cmdline, name)\n            }\n        } else {\n            (comm.clone(), comm)\n        }\n    };\n\n    // We have moved command processing here.\n    // SAFETY: We are only replacing a single char (NUL) with another single char (space).\n\n    let mut command = command;\n    let buf_mut = unsafe { command.as_mut_vec() };\n\n    for byte in buf_mut {\n        if *byte == 0 {\n            const SPACE: u8 = ' '.to_ascii_lowercase() as u8;\n            *byte = SPACE;\n        }\n    }\n\n    Ok((\n        ProcessHarvest {\n            pid: process.pid,\n            parent_pid,\n            cpu_usage_percent,\n            mem_usage_percent,\n            mem_usage,\n            virtual_mem,\n            name,\n            command,\n            read_per_sec,\n            write_per_sec,\n            total_read,\n            total_write,\n            process_state,\n            uid,\n            user,\n            time,\n            #[cfg(feature = \"gpu\")]\n            gpu_mem: 0,\n            #[cfg(feature = \"gpu\")]\n            gpu_mem_percent: 0.0,\n            #[cfg(feature = \"gpu\")]\n            gpu_util: 0,\n            process_type,\n            #[cfg(unix)]\n            nice: stat.nice,\n            priority: stat.priority,\n        },\n        new_process_times,\n    ))\n}\n\n/// We follow something similar to how htop does it to identify a valid name based on the cmdline.\n/// - https://github.com/htop-dev/htop/blob/bcb18ef82269c68d54a160290e5f8b2e939674ec/Process.c#L268 (kinda)\n/// - https://github.com/htop-dev/htop/blob/bcb18ef82269c68d54a160290e5f8b2e939674ec/Process.c#L573\n///\n/// Also note that cmdline is (for us) separated by \\0.\nfn binary_name_from_cmdline(cmdline: &str) -> String {\n    let mut start = 0;\n    let mut end = cmdline.len();\n\n    for (i, c) in cmdline.chars().enumerate() {\n        if c == '/' {\n            start = i + 1;\n        } else if c == '\\0' || c == ':' {\n            end = i;\n            break;\n        }\n    }\n\n    // Bit of a hack to handle cases like \"firefox -blah\"\n    let partial = cmdline.chars().skip(start).take(end - start).join(\"\");\n    partial\n        .split_once(\" -\")\n        .map(|(name, _)| name.to_string())\n        .unwrap_or_else(|| partial.to_string())\n}\n\npub(crate) struct PrevProc<'a> {\n    pub prev_idle: &'a mut f64,\n    pub prev_non_idle: &'a mut f64,\n}\n\npub(crate) struct ProcHarvestOptions {\n    pub use_current_cpu_total: bool,\n    pub unnormalized_cpu: bool,\n    pub get_process_threads: bool,\n}\n\nfn is_str_numeric(s: &str) -> bool {\n    s.chars().all(|c| c.is_ascii_digit())\n}\n\n/// General args to keep around for reading proc data.\n#[derive(Copy, Clone)]\npub(crate) struct ReadProcArgs {\n    pub use_current_cpu_total: bool,\n    pub cpu_usage: f64,\n    pub cpu_fraction: f64,\n    pub total_memory: u64,\n    pub time_difference_in_secs: u64,\n    pub system_uptime: u64,\n    pub get_process_threads: bool,\n}\n\npub(crate) fn linux_process_data(\n    collector: &mut DataCollector, time_difference_in_secs: u64,\n) -> CollectionResult<Vec<ProcessHarvest>> {\n    let total_memory = collector.total_memory();\n    let prev_proc = PrevProc {\n        prev_idle: &mut collector.prev_idle,\n        prev_non_idle: &mut collector.prev_non_idle,\n    };\n    let proc_harvest_options = ProcHarvestOptions {\n        use_current_cpu_total: collector.use_current_cpu_total,\n        unnormalized_cpu: collector.unnormalized_cpu,\n        get_process_threads: collector.get_process_threads,\n    };\n    let prev_process_details = &mut collector.prev_process_details;\n    let user_table = &mut collector.user_table;\n\n    let ProcHarvestOptions {\n        use_current_cpu_total,\n        unnormalized_cpu,\n        get_process_threads: get_threads,\n    } = proc_harvest_options;\n\n    let PrevProc {\n        prev_idle,\n        prev_non_idle,\n    } = prev_proc;\n\n    // TODO: [PROC THREADS] Add threads\n\n    let CpuUsage {\n        mut cpu_usage,\n        cpu_fraction,\n    } = cpu_usage_calculation(prev_idle, prev_non_idle)?;\n\n    if unnormalized_cpu {\n        let num_processors = collector.sys.system.cpus().len() as f64;\n\n        // Note we *divide* here because the later calculation divides `cpu_usage` - in\n        // effect, multiplying over the number of cores.\n        cpu_usage /= num_processors;\n    }\n\n    // TODO: Could maybe use a double buffer hashmap to avoid allocating this each time?\n    // e.g. we swap which is prev and which is new.\n    let mut seen_pids: HashSet<Pid> = HashSet::default();\n\n    // Note this will only return PIDs of _processes_, not threads. You can get those from /proc/<PID>/task though.\n    let pids = fs::read_dir(\"/proc\")?.flatten().filter_map(|dir| {\n        // Need to filter out non-PID entries.\n        if is_str_numeric(dir.file_name().to_string_lossy().trim()) {\n            Some(dir.path())\n        } else {\n            None\n        }\n    });\n\n    let args = ReadProcArgs {\n        use_current_cpu_total,\n        cpu_usage,\n        cpu_fraction,\n        total_memory,\n        time_difference_in_secs,\n        system_uptime: sysinfo::System::uptime(),\n        get_process_threads: get_threads,\n    };\n\n    // TODO: Maybe pre-allocate these buffers in the future w/ routine cleanup.\n    let mut buffer = String::new();\n    let mut process_threads_to_check = HashMap::default();\n\n    let mut process_vector: Vec<ProcessHarvest> = pids\n        .filter_map(|pid_path| {\n            if let Ok((process, threads)) =\n                Process::from_path(pid_path, &mut buffer, args.get_process_threads)\n            {\n                let pid = process.pid;\n                let prev_proc_details = prev_process_details.entry(pid).or_default();\n\n                #[cfg_attr(not(feature = \"gpu\"), expect(unused_mut))]\n                if let Ok((mut process_harvest, new_process_times)) =\n                    read_proc(prev_proc_details, process, args, user_table, None)\n                {\n                    #[cfg(feature = \"gpu\")]\n                    if let Some(gpus) = &collector.gpu_pids {\n                        gpus.iter().for_each(|gpu| {\n                            // add mem/util for all gpus to pid\n                            if let Some((mem, util)) = gpu.get(&pid) {\n                                process_harvest.gpu_mem += mem;\n                                process_harvest.gpu_util += util;\n                            }\n                        });\n                        if let Some(gpu_total_mem) = &collector.gpus_total_mem {\n                            process_harvest.gpu_mem_percent =\n                                (process_harvest.gpu_mem as f64 / *gpu_total_mem as f64 * 100.0)\n                                    as f32;\n                        }\n                    }\n\n                    prev_proc_details.cpu_time = new_process_times;\n                    prev_proc_details.total_read_bytes = process_harvest.total_read;\n                    prev_proc_details.total_write_bytes = process_harvest.total_write;\n\n                    if !threads.is_empty() {\n                        process_threads_to_check.insert(pid, threads);\n                    }\n\n                    seen_pids.insert(pid);\n                    return Some(process_harvest);\n                }\n            }\n\n            None\n        })\n        .collect();\n\n    // Get thread data.\n    for (pid, tid_paths) in process_threads_to_check {\n        for tid_path in tid_paths {\n            if let Ok((process, _)) = Process::from_path(tid_path, &mut buffer, false) {\n                let tid = process.pid;\n                let prev_proc_details = prev_process_details.entry(tid).or_default();\n\n                if let Ok((process_harvest, new_process_times)) =\n                    read_proc(prev_proc_details, process, args, user_table, Some(pid))\n                {\n                    prev_proc_details.cpu_time = new_process_times;\n                    prev_proc_details.total_read_bytes = process_harvest.total_read;\n                    prev_proc_details.total_write_bytes = process_harvest.total_write;\n\n                    seen_pids.insert(tid);\n                    process_vector.push(process_harvest);\n                }\n            }\n        }\n    }\n\n    // Clean up values we don't care about anymore.\n    prev_process_details.retain(|pid, _| seen_pids.contains(pid));\n\n    // Occasional garbage collection.\n    if collector.should_run_less_routine_tasks {\n        prev_process_details.shrink_to_fit();\n    }\n\n    // TODO: This might be more efficient to just separate threads into their own list, but for now this works so it\n    // fits with existing code.\n    Ok(process_vector)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_proc_cpu_parse() {\n        assert_eq!(\n            (100_f64, 200_f64),\n            fetch_cpu_usage(\"100 0 100 100\"),\n            \"Failed to properly calculate idle/non-idle for /proc/stat CPU with 4 values\"\n        );\n        assert_eq!(\n            (120_f64, 200_f64),\n            fetch_cpu_usage(\"100 0 100 100 20\"),\n            \"Failed to properly calculate idle/non-idle for /proc/stat CPU with 5 values\"\n        );\n        assert_eq!(\n            (120_f64, 230_f64),\n            fetch_cpu_usage(\"100 0 100 100 20 30\"),\n            \"Failed to properly calculate idle/non-idle for /proc/stat CPU with 6 values\"\n        );\n        assert_eq!(\n            (120_f64, 270_f64),\n            fetch_cpu_usage(\"100 0 100 100 20 30 40\"),\n            \"Failed to properly calculate idle/non-idle for /proc/stat CPU with 7 values\"\n        );\n        assert_eq!(\n            (120_f64, 320_f64),\n            fetch_cpu_usage(\"100 0 100 100 20 30 40 50\"),\n            \"Failed to properly calculate idle/non-idle for /proc/stat CPU with 8 values\"\n        );\n        assert_eq!(\n            (120_f64, 320_f64),\n            fetch_cpu_usage(\"100 0 100 100 20 30 40 50 100\"),\n            \"Failed to properly calculate idle/non-idle for /proc/stat CPU with 9 values\"\n        );\n        assert_eq!(\n            (120_f64, 320_f64),\n            fetch_cpu_usage(\"100 0 100 100 20 30 40 50 100 200\"),\n            \"Failed to properly calculate idle/non-idle for /proc/stat CPU with 10 values\"\n        );\n    }\n\n    #[test]\n    fn test_name_from_cmdline() {\n        assert_eq!(binary_name_from_cmdline(\"/usr/bin/btm\"), \"btm\");\n        assert_eq!(\n            binary_name_from_cmdline(\"/usr/bin/btm\\0--asdf\\0--asdf/gkj\"),\n            \"btm\"\n        );\n        assert_eq!(binary_name_from_cmdline(\"/usr/bin/btm:\"), \"btm\");\n        assert_eq!(binary_name_from_cmdline(\"/usr/bin/b tm\"), \"b tm\");\n        assert_eq!(binary_name_from_cmdline(\"/usr/bin/b tm:\"), \"b tm\");\n        assert_eq!(binary_name_from_cmdline(\"/usr/bin/b tm\\0--test\"), \"b tm\");\n        assert_eq!(binary_name_from_cmdline(\"/usr/bin/b tm:\\0--test\"), \"b tm\");\n        assert_eq!(\n            binary_name_from_cmdline(\"/usr/bin/b t m:\\0--\\\"test thing\\\"\"),\n            \"b t m\"\n        );\n        assert_eq!(\n            binary_name_from_cmdline(\"firefox -contentproc -isForBrowser -prefsHandle 0\"),\n            \"firefox\"\n        );\n        assert_eq!(binary_name_from_cmdline(\"こんにちは\\0\"), \"こんにちは\");\n        assert_eq!(\n            binary_name_from_cmdline(\"こんにちは -こんばんは\"),\n            \"こんにちは\"\n        );\n        assert_eq!(\n            binary_name_from_cmdline(\"/usr/bin/こんにちは -こんばんは\\0\"),\n            \"こんにちは\"\n        );\n    }\n}\n"
  },
  {
    "path": "src/collection/processes/linux/process.rs",
    "content": "//! Linux process code for getting process data via `/proc/`.\n//! Based on the [procfs](https://github.com/eminence/procfs) crate.\n\nuse std::{\n    fs::File,\n    io::{self, BufRead, BufReader, Read},\n    path::PathBuf,\n    sync::OnceLock,\n};\n\nuse anyhow::anyhow;\nuse libc::uid_t;\nuse rustix::{\n    fd::OwnedFd,\n    fs::{Mode, OFlags},\n    path::Arg,\n};\n\nuse crate::collection::processes::{Pid, linux::is_str_numeric};\n\nstatic PAGESIZE: OnceLock<u64> = OnceLock::new();\n\n#[inline]\nfn next_part<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Result<&'a str, io::Error> {\n    iter.next()\n        .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))\n}\n\n/// A wrapper around the data in `/proc/<PID>/stat`. For documentation, see:\n/// - <https://manpages.ubuntu.com/manpages/noble/man5/proc_pid_stat.5.html>\n/// - <https://man7.org/linux/man-pages/man5/proc_pid_status.5.html>\n///\n/// Note this does not necessarily get all fields, only the ones we use in\n/// bottom.\npub(crate) struct Stat {\n    /// The filename of the executable without parentheses.\n    pub comm: String,\n\n    /// The current process state, represented by a char.\n    pub state: char,\n\n    /// The parent process PID.\n    pub ppid: Pid,\n\n    /// The amount of time this process has been scheduled in user mode in clock\n    /// ticks.\n    pub utime: u64,\n\n    /// The amount of time this process has been scheduled in kernel mode in\n    /// clock ticks.\n    pub stime: u64,\n\n    /// The resident set size, or the number of pages the process has in real\n    /// memory.\n    rss: u64,\n\n    /// The virtual memory size in bytes.\n    pub vsize: u64,\n\n    /// The start time of the process, represented in clock ticks.\n    pub start_time: u64,\n\n    /// Kernel thread\n    pub is_kernel_thread: bool,\n\n    /// The kernel scheduling priority.\n    pub priority: i32,\n\n    /// The nice value (user-settable scheduling hint).\n    #[cfg(unix)]\n    pub nice: i32,\n}\n\nimpl Stat {\n    /// Get process stats from a file; this assumes the file is located at\n    /// `/proc/<PID>/stat`. For documentation, see\n    /// [here](https://manpages.ubuntu.com/manpages/noble/man5/proc_pid_stat.5.html) as a reference.\n    fn from_file(mut f: File, buffer: &mut String) -> anyhow::Result<Stat> {\n        // Since this is just one line, we can read it all at once. However, since it\n        // (technically) might have non-utf8 characters, we can't just use read_to_string.\n        f.read_to_end(unsafe { buffer.as_mut_vec() })?;\n\n        // TODO: Is this needed?\n        let line = buffer.trim();\n\n        let (comm, rest) = {\n            let start_paren = line\n                .find('(')\n                .ok_or_else(|| anyhow!(\"start paren missing\"))?;\n            let end_paren = line.find(')').ok_or_else(|| anyhow!(\"end paren missing\"))?;\n\n            (\n                line[start_paren + 1..end_paren].to_string(),\n                &line[end_paren + 2..],\n            )\n        };\n\n        let mut rest = rest.split(' ');\n        let state = next_part(&mut rest)?\n            .chars()\n            .next()\n            .ok_or_else(|| anyhow!(\"missing state\"))?;\n        let ppid: Pid = next_part(&mut rest)?.parse()?;\n\n        // Skip 4 fields (pgrp, session, tty_nr, tpgid)\n        let mut rest = rest.skip(4);\n\n        // read flags for kernel thread (PF_KTHREAD from include/linux/sched.h)\n        let flags: u32 = next_part(&mut rest)?.parse()?;\n        let is_kernel_thread: bool = flags & 0x00200000 != 0;\n\n        // Skip 4 fields (minflt, cminflt, majflt, cmajflt)\n        let mut rest = rest.skip(4);\n        let utime: u64 = next_part(&mut rest)?.parse()?;\n        let stime: u64 = next_part(&mut rest)?.parse()?;\n\n        // cutime\n        let _ = next_part(&mut rest)?;\n        // cstime\n        let _ = next_part(&mut rest)?;\n        // priority\n        let priority: i32 = next_part(&mut rest)?.parse()?;\n        // nice\n        let nice: i32 = next_part(&mut rest)?.parse()?;\n        // num_threads\n        let _ = next_part(&mut rest)?;\n        // itrealvalue\n        let _ = next_part(&mut rest)?;\n\n        let start_time: u64 = next_part(&mut rest)?.parse()?;\n        let vsize: u64 = next_part(&mut rest)?.parse()?;\n        let rss: u64 = next_part(&mut rest)?.parse()?;\n\n        Ok(Stat {\n            comm,\n            state,\n            ppid,\n            utime,\n            stime,\n            rss,\n            vsize,\n            start_time,\n            is_kernel_thread,\n            priority,\n            nice,\n        })\n    }\n\n    /// Returns the Resident Set Size in bytes.\n    #[inline]\n    pub fn rss_bytes(&self) -> u64 {\n        self.rss * PAGESIZE.get_or_init(|| rustix::param::page_size() as u64)\n    }\n}\n\n/// A wrapper around the data in `/proc/<PID>/io`.\n///\n/// Note this does not necessarily get all fields, only the ones we use in\n/// bottom.\npub(crate) struct Io {\n    pub read_bytes: u64,\n    pub write_bytes: u64,\n}\n\nimpl Io {\n    #[inline]\n    fn from_file(f: File, buffer: &mut String) -> anyhow::Result<Io> {\n        const NUM_FIELDS: u16 = 0; // Make sure to update this if you want more fields!\n        enum Fields {\n            ReadBytes,\n            WriteBytes,\n        }\n\n        let mut read_fields = 0;\n        let mut reader = BufReader::new(f);\n\n        let mut read_bytes = 0;\n        let mut write_bytes = 0;\n\n        // This saves us from doing a string allocation on each iteration compared to\n        // `lines()`.\n        while let Ok(bytes) = reader.read_line(buffer) {\n            if bytes > 0 {\n                if buffer.is_empty() {\n                    // Empty, no need to clear.\n                    continue;\n                }\n\n                let mut parts = buffer.split_whitespace();\n\n                if let Some(field) = parts.next() {\n                    let curr_field = match field {\n                        \"read_bytes:\" => Fields::ReadBytes,\n                        \"write_bytes:\" => Fields::WriteBytes,\n                        _ => {\n                            buffer.clear();\n                            continue;\n                        }\n                    };\n\n                    if let Some(value) = parts.next() {\n                        let value = value.parse::<u64>()?;\n                        match curr_field {\n                            Fields::ReadBytes => {\n                                read_bytes = value;\n                                read_fields += 1;\n                            }\n                            Fields::WriteBytes => {\n                                write_bytes = value;\n                                read_fields += 1;\n                            }\n                        }\n                    }\n                }\n\n                // Quick short circuit if we have already read all the required fields.\n                if read_fields == NUM_FIELDS {\n                    break;\n                }\n\n                buffer.clear();\n            } else {\n                break;\n            }\n        }\n\n        Ok(Io {\n            read_bytes,\n            write_bytes,\n        })\n    }\n}\n\n/// A wrapper around a Linux process operations in `/proc/<PID>`.\n///\n/// Core documentation based on [proc's manpages](https://man7.org/linux/man-pages/man5/proc.5.html).\npub(crate) struct Process {\n    pub pid: Pid,\n    pub uid: Option<uid_t>,\n    pub stat: Stat,\n    pub io: Option<Io>,\n    pub cmdline: Option<String>,\n}\n\n#[inline]\nfn reset(root: &mut PathBuf, buffer: &mut String) {\n    root.pop();\n    buffer.clear();\n}\n\nimpl Process {\n    /// Creates a new [`Process`] given a `/proc/<PID>` path. This may fail if\n    /// the process no longer exists or there are permissions issues.\n    ///\n    /// Note that this pre-allocates fields on **creation**! As such, some data\n    /// might end up \"outdated\" depending on when you call some of the\n    /// methods. Therefore, this struct is only useful for either fields\n    /// that are unlikely to change, or are short-lived and\n    /// will be discarded quickly.\n    ///\n    /// This takes in a buffer to avoid allocs; this function will clear the buffer.\n    #[inline]\n    pub(crate) fn from_path(\n        pid_path: PathBuf, buffer: &mut String, get_threads: bool,\n    ) -> anyhow::Result<(Process, Vec<PathBuf>)> {\n        buffer.clear();\n\n        let pid_dir = rustix::fs::openat(\n            rustix::fs::CWD,\n            pid_path.as_path(),\n            OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,\n            Mode::empty(),\n        )?;\n\n        let pid = pid_path\n            .as_path()\n            .components()\n            .next_back()\n            .and_then(|s| s.to_string_lossy().parse::<Pid>().ok())\n            .or_else(|| {\n                rustix::fs::readlinkat(rustix::fs::CWD, pid_path.as_path(), vec![])\n                    .ok()\n                    .and_then(|s| s.to_string_lossy().parse::<Pid>().ok())\n            })\n            .ok_or_else(|| anyhow!(\"PID for {pid_path:?} was not found\"))?;\n\n        let uid = {\n            let metadata = rustix::fs::fstat(&pid_dir);\n            match metadata {\n                Ok(md) => Some(md.st_uid),\n                Err(_) => None,\n            }\n        };\n\n        let mut root = pid_path;\n\n        // NB: Whenever you add a new stat, make sure to pop the root and clear the\n        // buffer!\n\n        // Stat is pretty long, do this first to pre-allocate up-front.\n        let stat =\n            open_at(&mut root, \"stat\", &pid_dir).and_then(|file| Stat::from_file(file, buffer))?;\n        reset(&mut root, buffer);\n\n        let cmdline = if cmdline(&mut root, &pid_dir, buffer).is_ok() {\n            // The clone will give a string with the capacity of the length of buffer, don't worry.\n            Some(buffer.clone())\n        } else {\n            None\n        };\n        reset(&mut root, buffer);\n\n        let io = open_at(&mut root, \"io\", &pid_dir)\n            .and_then(|file| Io::from_file(file, buffer))\n            .ok();\n\n        reset(&mut root, buffer);\n\n        let threads = threads(&mut root, pid, get_threads);\n\n        Ok((\n            Process {\n                pid,\n                uid,\n                stat,\n                io,\n                cmdline,\n            },\n            threads,\n        ))\n    }\n}\n\n#[inline]\nfn cmdline(root: &mut PathBuf, fd: &OwnedFd, buffer: &mut String) -> anyhow::Result<()> {\n    let _ = open_at(root, \"cmdline\", fd).map(|mut file| file.read_to_string(buffer))?;\n\n    Ok(())\n}\n\n/// Opens a path. Note that this function takes in a mutable root - this will\n/// mutate it to avoid allocations. You probably will want to pop the most\n/// recent child after if you need to use the buffer again.\n#[inline]\nfn open_at(root: &mut PathBuf, child: &str, fd: &OwnedFd) -> anyhow::Result<File> {\n    root.push(child);\n    let new_fd = rustix::fs::openat(fd, &*root, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())?;\n\n    Ok(File::from(new_fd))\n}\n\n#[inline]\nfn threads(root: &mut PathBuf, pid: Pid, get_threads: bool) -> Vec<PathBuf> {\n    if get_threads {\n        root.push(\"task\");\n\n        let Ok(task_dir) = rustix::fs::openat(\n            rustix::fs::CWD,\n            root.as_path(),\n            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,\n            Mode::empty(),\n        ) else {\n            return Vec::new();\n        };\n\n        if let Ok(task) = rustix::fs::Dir::read_from(task_dir) {\n            let pid_str = pid.to_string();\n\n            return task\n                .flatten()\n                .filter_map(|thread_dir| {\n                    let file_name = thread_dir.file_name();\n                    let file_name = file_name.to_string_lossy();\n                    let file_name = file_name.trim();\n\n                    if is_str_numeric(file_name) && file_name != pid_str {\n                        Some(root.join(file_name).to_path_buf())\n                    } else {\n                        None\n                    }\n                })\n                .collect::<Vec<_>>();\n        }\n    }\n\n    Vec::new()\n}\n"
  },
  {
    "path": "src/collection/processes/macos/sysctl_bindings.rs",
    "content": "//! Partial bindings from Apple's open source code for getting process\n//! information. Some of this is based on [heim's binding implementation](https://github.com/heim-rs/heim/blob/master/heim-process/src/sys/macos/bindings/process.rs).\n\nuse std::mem;\n\nuse anyhow::{Result, bail};\nuse libc::{\n    CTL_KERN, KERN_PROC, KERN_PROC_PID, MAXCOMLEN, boolean_t, c_char, c_long, c_short, c_uchar,\n    c_ushort, c_void, dev_t, gid_t, itimerval, pid_t, rusage, sigset_t, timeval, uid_t, xucred,\n};\nuse mach2::vm_types::user_addr_t;\n\nuse crate::collection::Pid;\n\n#[repr(C)]\npub(crate) struct kinfo_proc {\n    pub kp_proc: extern_proc,\n    pub kp_eproc: eproc,\n}\n\n#[repr(C)]\n#[derive(Copy, Clone)]\npub struct p_st1 {\n    /// Doubly-linked run/sleep queue.\n    p_forw: user_addr_t,\n    p_back: user_addr_t,\n}\n\n#[repr(C)]\npub union p_un {\n    pub p_st1: p_st1,\n\n    /// process start time\n    pub p_starttime: timeval,\n}\n\n/// Exported fields for kern sysctl. See\n/// [`proc.h`](https://opensource.apple.com/source/xnu/xnu-201/bsd/sys/proc.h)\n#[repr(C)]\npub(crate) struct extern_proc {\n    pub p_un: p_un,\n\n    /// Address space.\n    pub p_vmspace: *mut vmspace,\n\n    /// Signal actions, state (PROC ONLY). Should point to\n    /// a `sigacts` but we don't really seem to need this.\n    pub p_sigacts: user_addr_t,\n\n    /// P_* flags.\n    pub p_flag: i32,\n\n    /// S* process status.\n    pub p_stat: c_char,\n\n    /// Process identifier.\n    pub p_pid: pid_t,\n\n    /// Save parent pid during ptrace.\n    pub p_oppid: pid_t,\n\n    /// Sideways return value from fdopen.\n    pub p_dupfd: i32,\n\n    /// where user stack was allocated\n    pub user_stack: caddr_t,\n\n    /// Which thread is exiting?\n    pub exit_thread: *mut c_void,\n\n    /// allow to debug\n    pub p_debugger: i32,\n\n    /// indication to suspend\n    pub sigwait: boolean_t,\n\n    /// Time averaged value of p_cpticks.\n    pub p_estcpu: u32,\n\n    /// Ticks of cpu time.\n    pub p_cpticks: i32,\n\n    /// %cpu for this process during p_swtime\n    pub p_pctcpu: fixpt_t,\n\n    /// Sleep address.\n    pub p_wchan: *mut c_void,\n\n    /// Reason for sleep.\n    pub p_wmesg: *mut c_char,\n\n    /// Time swapped in or out.\n    pub p_swtime: u32,\n\n    /// Time since last blocked.\n    pub p_slptime: u32,\n\n    /// Alarm timer.\n    pub p_realtimer: itimerval,\n\n    /// Real time.\n    pub p_rtime: timeval,\n\n    /// Statclock hit in user mode.\n    pub p_uticks: u64,\n\n    /// Statclock hits in system mode.\n    pub p_sticks: u64,\n\n    /// Statclock hits processing intr.\n    pub p_iticks: u64,\n\n    /// Kernel trace points.\n    pub p_traceflag: i32,\n\n    /// Trace to vnode. Originally a pointer to a struct of vnode.\n    pub p_tracep: *mut c_void,\n\n    /// DEPRECATED.\n    pub p_siglist: i32,\n\n    /// Vnode of executable. Originally a pointer to a struct of vnode.\n    pub p_textvp: *mut c_void,\n\n    /// If non-zero, don't swap.\n    pub p_holdcnt: i32,\n\n    /// DEPRECATED.\n    pub p_sigmask: sigset_t,\n\n    /// Signals being ignored.\n    pub p_sigignore: sigset_t,\n\n    /// Signals being caught by user.\n    pub p_sigcatch: sigset_t,\n\n    /// Process priority.\n    pub p_priority: c_uchar,\n\n    /// User-priority based on p_cpu and p_nice.\n    pub p_usrpri: c_uchar,\n\n    /// Process \"nice\" value.\n    pub p_nice: c_char,\n\n    pub p_comm: [c_char; MAXCOMLEN + 1],\n\n    /// Pointer to process group. Originally a pointer to a `pgrp`.\n    pub p_pgrp: *mut c_void,\n\n    /// Kernel virtual addr of u-area (PROC ONLY). Originally a pointer to a\n    /// `user`.\n    pub p_addr: *mut c_void,\n\n    /// Exit status for wait; also stop signal.\n    pub p_xstat: c_ushort,\n\n    /// Accounting flags.\n    pub p_acflag: c_ushort,\n\n    /// Exit information. XXX\n    pub p_ru: *mut rusage,\n}\n\nconst WMESGLEN: usize = 7;\nconst COMAPT_MAXLOGNAME: usize = 12;\n\n/// See `_caddr_t.h`.\n#[expect(non_camel_case_types)]\ntype caddr_t = *const libc::c_char;\n\n/// See `types.h`.\n#[expect(non_camel_case_types)]\ntype segsz_t = i32;\n\n/// See `types.h`.\n#[expect(non_camel_case_types)]\ntype fixpt_t = u32;\n\n/// See [`proc.h`](https://opensource.apple.com/source/xnu/xnu-201/bsd/sys/proc.h)\n#[repr(C)]\npub(crate) struct pcred {\n    pub pc_lock: [c_char; 72],\n    pub pc_ucred: *mut xucred,\n    pub p_ruid: uid_t,\n    pub p_svuid: uid_t,\n    pub p_rgid: gid_t,\n    pub p_svgid: gid_t,\n    pub p_refcnt: i32,\n}\n\n/// See `vm.h`.\n#[repr(C)]\npub(crate) struct vmspace {\n    pub dummy: i32,\n    pub dummy2: caddr_t,\n    pub dummy3: [i32; 5],\n    pub dummy4: [caddr_t; 3],\n}\n\n/// See [`sysctl.h`](https://opensource.apple.com/source/xnu/xnu-344/bsd/sys/sysctl.h).\n#[repr(C)]\npub(crate) struct eproc {\n    /// Address of proc. We just cheat and use a c_void pointer since we aren't\n    /// using this.\n    pub e_paddr: *mut c_void,\n\n    /// Session pointer.  We just cheat and use a c_void pointer since we aren't\n    /// using this.\n    pub e_sess: *mut c_void,\n\n    /// Process credentials\n    pub e_pcred: pcred,\n\n    /// Current credentials\n    pub e_ucred: xucred,\n\n    /// Address space\n    pub e_vm: vmspace,\n\n    /// Parent process ID\n    pub e_ppid: pid_t,\n\n    /// Process group ID\n    pub e_pgid: pid_t,\n\n    /// Job control counter\n    pub e_jobc: c_short,\n\n    /// Controlling tty dev\n    pub e_tdev: dev_t,\n\n    /// tty process group id\n    pub e_tpgid: pid_t,\n\n    /// tty session pointer.  We just cheat and use a c_void pointer since we\n    /// aren't using this.\n    pub e_tsess: *mut c_void,\n\n    /// wchan message\n    pub e_wmesg: [c_char; WMESGLEN + 1],\n\n    /// text size\n    pub e_xsize: segsz_t,\n\n    /// text rss\n    pub e_xrssize: c_short,\n\n    /// text references\n    pub e_xccount: c_short,\n\n    pub e_xswrss: c_short,\n\n    pub e_flag: c_long,\n\n    /// short setlogin() name\n    pub e_login: [c_char; COMAPT_MAXLOGNAME],\n\n    pub e_spare: [c_long; 4],\n}\n\n/// Obtains the [`kinfo_proc`] given a process PID.\n///\n/// Based on the implementation from [heim](https://github.com/heim-rs/heim/blob/master/heim-process/src/sys/macos/bindings/process.rs#L235).\npub(crate) fn kinfo_process(pid: Pid) -> Result<kinfo_proc> {\n    let mut name: [i32; 4] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid];\n    let mut size = mem::size_of::<kinfo_proc>();\n    let mut info = mem::MaybeUninit::<kinfo_proc>::uninit();\n\n    // SAFETY: libc binding, we assume all arguments are valid.\n    let result = unsafe {\n        libc::sysctl(\n            name.as_mut_ptr(),\n            4,\n            info.as_mut_ptr() as *mut libc::c_void,\n            &mut size,\n            std::ptr::null_mut(),\n            0,\n        )\n    };\n\n    if result < 0 {\n        bail!(\"failed to get process for pid {pid}\");\n    }\n\n    // sysctl succeeds but size is zero, happens when process has gone away\n    if size == 0 {\n        bail!(\"failed to get process for pid {pid}\");\n    }\n\n    // SAFETY: info is initialized if result succeeded and returned a non-negative\n    // result. If sysctl failed, it returns -1 with errno set.\n    //\n    // Source: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/sysctl.3.html\n    unsafe { Ok(info.assume_init()) }\n}\n\n#[cfg(test)]\nmod test {\n    use std::mem;\n\n    use super::*;\n\n    /// A quick test to ensure that things are sized correctly.\n    #[test]\n    fn test_struct_sizes() {\n        assert_eq!(mem::size_of::<p_st1>(), 16);\n        assert_eq!(mem::align_of::<p_st1>(), 8);\n\n        assert_eq!(mem::size_of::<pcred>(), 104);\n        assert_eq!(mem::align_of::<pcred>(), 8);\n\n        assert_eq!(mem::size_of::<vmspace>(), 64);\n        assert_eq!(mem::align_of::<vmspace>(), 8);\n\n        assert_eq!(mem::size_of::<extern_proc>(), 296);\n        assert_eq!(mem::align_of::<extern_proc>(), 8);\n\n        assert_eq!(mem::size_of::<eproc>(), 376);\n        assert_eq!(mem::align_of::<eproc>(), 8);\n\n        assert_eq!(mem::size_of::<kinfo_proc>(), 672);\n        assert_eq!(mem::align_of::<kinfo_proc>(), 8);\n    }\n}\n"
  },
  {
    "path": "src/collection/processes/macos.rs",
    "content": "//! Process data collection for macOS.  Uses sysinfo and custom bindings.\n\npub mod sysctl_bindings;\n\nuse std::{io, process::Command};\n\nuse itertools::Itertools;\nuse nohash::IntMap;\n\nuse super::UnixProcessExt;\nuse crate::collection::Pid;\n\npub(crate) struct MacOSProcessExt;\n\nimpl UnixProcessExt for MacOSProcessExt {\n    #[inline]\n    fn has_backup_proc_cpu_fn() -> bool {\n        true\n    }\n\n    fn backup_proc_cpu(pids: &[Pid]) -> io::Result<IntMap<Pid, f32>> {\n        let output = Command::new(\"ps\")\n            .args([\"-o\", \"pid=,pcpu=\", \"-p\"])\n            .arg(\n                // Has to look like this since otherwise, it you hit a `unstable_name_collisions`\n                // warning.\n                Itertools::intersperse(pids.iter().map(i32::to_string), \",\".to_string())\n                    .collect::<String>(),\n            )\n            .output()?;\n        let mut result = IntMap::default();\n        String::from_utf8_lossy(&output.stdout)\n            .split_whitespace()\n            .chunks(2)\n            .into_iter()\n            .for_each(|chunk| {\n                let chunk: Vec<&str> = chunk.collect();\n                if chunk.len() != 2 {\n                    panic!(\"Unexpected 'ps' output\");\n                }\n                let pid = chunk[0].parse();\n                let usage = chunk[1].parse();\n                if let (Ok(pid), Ok(usage)) = (pid, usage) {\n                    result.insert(pid, usage);\n                }\n            });\n        Ok(result)\n    }\n\n    fn parent_pid(process_val: &sysinfo::Process) -> Option<Pid> {\n        process_val\n            .parent()\n            .map(|p| p.as_u32() as _)\n            .or_else(|| fallback_macos_ppid(process_val.pid().as_u32() as _))\n    }\n}\n\nfn fallback_macos_ppid(pid: Pid) -> Option<Pid> {\n    sysctl_bindings::kinfo_process(pid)\n        .map(|kinfo| kinfo.kp_eproc.e_ppid)\n        .ok()\n}\n"
  },
  {
    "path": "src/collection/processes/unix/process_ext.rs",
    "content": "//! Shared process data harvesting code from macOS and FreeBSD via sysinfo.\n\nuse std::{io, time::Duration};\n\nuse cfg_if::cfg_if;\nuse itertools::Itertools;\nuse nohash::IntMap;\nuse sysinfo::{ProcessStatus, System};\n\nuse super::{ProcessHarvest, process_status_str};\n#[cfg(target_os = \"macos\")]\nuse crate::collection::processes::macos::sysctl_bindings;\nuse crate::collection::{Pid, error::CollectionResult, processes::UserTable};\n\nfn get_nice(pid: Pid) -> i32 {\n    // SAFETY: getpriority takes no user pointers; pid is passed as a value\n    // and errors are reported via the return value.\n    cfg_if! {\n        if #[cfg(target_os = \"freebsd\")] {\n            unsafe { libc::getpriority(libc::PRIO_PROCESS, pid) }\n        } else if #[cfg(target_os = \"macos\")] {\n            unsafe { libc::getpriority(libc::PRIO_PROCESS, pid as u32) }\n        } else {\n            0\n        }\n    }\n}\n\nfn get_priority(pid: Pid) -> i32 {\n    cfg_if! {\n        if #[cfg(target_os = \"macos\")] {\n            if let Ok(kinfo) = sysctl_bindings::kinfo_process(pid) {\n                kinfo.kp_proc.p_priority as i32\n            } else {\n                0\n            }\n        } else if #[cfg(target_os = \"freebsd\")] {\n            use libc::{c_int, c_void};\n            use std::{mem, ptr};\n\n            let mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid as c_int];\n            let mut kp: libc::kinfo_proc = unsafe { mem::zeroed() };\n            let mut size = mem::size_of::<libc::kinfo_proc>();\n\n            // SAFETY: sysctl takes the following pointer arguments\n            // - mib is valid for KERN_PROC_PID.\n            // - kp is a properly sized output buffer.\n            // - newp is null for a read-only sysctl.\n            let ret = unsafe {\n                libc::sysctl(\n                    mib.as_ptr(),\n                    mib.len() as u32,\n                    &mut kp as *mut _ as *mut c_void,\n                    &mut size,\n                    ptr::null_mut(),\n                    0,\n                )\n            };\n\n            if ret == 0 { kp.ki_pri.pri_level as i32 } else { 0 }\n        } else {\n            0\n        }\n    }\n}\n\npub(crate) trait UnixProcessExt {\n    fn sysinfo_process_data(\n        sys: &System, use_current_cpu_total: bool, unnormalized_cpu: bool, total_memory: u64,\n        user_table: &mut UserTable,\n    ) -> CollectionResult<Vec<ProcessHarvest>> {\n        let mut process_vector: Vec<ProcessHarvest> = Vec::new();\n        let process_hashmap = sys.processes();\n        let cpu_usage = sys.global_cpu_usage() / 100.0;\n        let num_processors = sys.cpus().len();\n\n        for process_val in process_hashmap.values() {\n            let name = if process_val.name().is_empty() {\n                let process_cmd = process_val.cmd();\n                if let Some(name) = process_cmd.first() {\n                    name.to_string_lossy().to_string()\n                } else {\n                    process_val\n                        .exe()\n                        .and_then(|exe| exe.file_stem())\n                        .and_then(|stem| stem.to_str())\n                        .map(|s| s.to_string())\n                        .unwrap_or(String::new())\n                }\n            } else {\n                process_val.name().to_string_lossy().to_string()\n            };\n            let command = {\n                let command = process_val\n                    .cmd()\n                    .iter()\n                    .map(|s| s.to_string_lossy())\n                    .join(\" \");\n                if command.is_empty() {\n                    name.clone()\n                } else {\n                    command\n                }\n            };\n\n            let pcu = {\n                let usage = process_val.cpu_usage();\n                if unnormalized_cpu || num_processors == 0 {\n                    usage\n                } else {\n                    usage / num_processors as f32\n                }\n            };\n            let process_cpu_usage = if use_current_cpu_total && cpu_usage > 0.0 {\n                pcu / cpu_usage\n            } else {\n                pcu\n            };\n\n            let disk_usage = process_val.disk_usage();\n            let process_state = {\n                let ps = process_val.status();\n                (process_status_str(ps), convert_process_status_to_char(ps))\n            };\n            let uid = process_val.user_id().map(|u| **u);\n            let pid = process_val.pid().as_u32() as Pid;\n            let nice = get_nice(pid);\n            let priority = get_priority(pid);\n\n            process_vector.push(ProcessHarvest {\n                pid,\n                parent_pid: Self::parent_pid(process_val),\n                name,\n                command,\n                mem_usage_percent: if total_memory > 0 {\n                    (process_val.memory() as f64 * 100.0 / total_memory as f64) as f32\n                } else {\n                    0.0\n                },\n                mem_usage: process_val.memory(),\n                virtual_mem: process_val.virtual_memory(),\n                cpu_usage_percent: process_cpu_usage,\n                read_per_sec: disk_usage.read_bytes,\n                write_per_sec: disk_usage.written_bytes,\n                total_read: disk_usage.total_read_bytes,\n                total_write: disk_usage.total_written_bytes,\n                process_state,\n                uid,\n                user: uid.and_then(|uid| user_table.uid_to_username(uid).ok()),\n                time: if process_val.start_time() == 0 {\n                    Duration::ZERO\n                } else {\n                    Duration::from_secs(process_val.run_time())\n                },\n                #[cfg(feature = \"gpu\")]\n                gpu_mem: 0,\n                #[cfg(feature = \"gpu\")]\n                gpu_mem_percent: 0.0,\n                #[cfg(feature = \"gpu\")]\n                gpu_util: 0,\n                #[cfg(unix)]\n                nice,\n                priority,\n            });\n        }\n\n        if Self::has_backup_proc_cpu_fn() {\n            let unknown_state = ProcessStatus::Unknown(0).to_string();\n            let cpu_usage_unknown_pids: Vec<Pid> = process_vector\n                .iter()\n                .filter(|process| process.process_state.0 == unknown_state)\n                .map(|process| process.pid)\n                .collect();\n            let cpu_usages = Self::backup_proc_cpu(&cpu_usage_unknown_pids)?;\n            for process in &mut process_vector {\n                if let Some(&cpu_usage) = cpu_usages.get(&process.pid) {\n                    process.cpu_usage_percent = if unnormalized_cpu || num_processors == 0 {\n                        cpu_usage\n                    } else {\n                        cpu_usage / num_processors as f32\n                    };\n                }\n            }\n        }\n\n        Ok(process_vector)\n    }\n\n    #[inline]\n    fn has_backup_proc_cpu_fn() -> bool {\n        false\n    }\n\n    fn backup_proc_cpu(_pids: &[Pid]) -> io::Result<IntMap<Pid, f32>> {\n        Ok(IntMap::default())\n    }\n\n    fn parent_pid(process_val: &sysinfo::Process) -> Option<Pid> {\n        process_val.parent().map(|p| p.as_u32() as _)\n    }\n}\n\nfn convert_process_status_to_char(status: ProcessStatus) -> char {\n    // TODO: Based on https://github.com/GuillaumeGomez/sysinfo/blob/baa46efb46d82f21b773088603720262f4a34646/src/unix/freebsd/process.rs#L13?\n    cfg_if::cfg_if! {\n        if #[cfg(target_os = \"macos\")] {\n            // SAFETY: These are all const and should be valid characters.\n            const SIDL: char = unsafe { char::from_u32_unchecked(libc::SIDL) };\n\n            // SAFETY: These are all const and should be valid characters.\n            const SRUN: char = unsafe { char::from_u32_unchecked(libc::SRUN) };\n\n            // SAFETY: These are all const and should be valid characters.\n            const SSLEEP: char = unsafe { char::from_u32_unchecked(libc::SSLEEP) };\n\n            // SAFETY: These are all const and should be valid characters.\n            const SSTOP: char = unsafe { char::from_u32_unchecked(libc::SSTOP) };\n\n            // SAFETY: These are all const and should be valid characters.\n            const SZOMB: char = unsafe { char::from_u32_unchecked(libc::SZOMB) };\n\n            match status {\n                ProcessStatus::Idle => SIDL,\n                ProcessStatus::Run => SRUN,\n                ProcessStatus::Sleep => SSLEEP,\n                ProcessStatus::Stop => SSTOP,\n                ProcessStatus::Zombie => SZOMB,\n                _ => '?'\n            }\n        } else if #[cfg(target_os = \"freebsd\")] {\n            const fn assert_u8(val: libc::c_char) -> u8 {\n                if val < 0 { panic!(\"there was an invalid i8 constant that is supposed to be a char\") } else { val as u8 }\n            }\n\n            const SIDL: u8 = assert_u8(libc::SIDL);\n            const SRUN: u8 = assert_u8(libc::SRUN);\n            const SSLEEP: u8 = assert_u8(libc::SSLEEP);\n            const SSTOP: u8 = assert_u8(libc::SSTOP);\n            const SZOMB: u8 = assert_u8(libc::SZOMB);\n            const SWAIT: u8 = assert_u8(libc::SWAIT);\n            const SLOCK: u8 = assert_u8(libc::SLOCK);\n\n            match status {\n                ProcessStatus::Idle => SIDL as char,\n                ProcessStatus::Run => SRUN as char,\n                ProcessStatus::Sleep => SSLEEP as char,\n                ProcessStatus::Stop => SSTOP as char,\n                ProcessStatus::Zombie => SZOMB as char,\n                ProcessStatus::Dead => SWAIT as char,\n                ProcessStatus::LockBlocked => SLOCK as char,\n                _ => '?'\n            }\n        } else {\n            '?'\n        }\n    }\n}\n"
  },
  {
    "path": "src/collection/processes/unix/user_table.rs",
    "content": "use std::sync::Arc;\n\nuse rustc_hash::FxHashMap as HashMap;\n\nuse crate::collection::error::{CollectionError, CollectionResult};\n\n#[derive(Debug, Default)]\npub struct UserTable {\n    pub uid_user_mapping: HashMap<libc::uid_t, Arc<str>>,\n}\n\nimpl UserTable {\n    /// Get the username associated with a UID. On first access of a name, it will\n    /// be cached for future accesses.\n    pub fn uid_to_username(&mut self, uid: libc::uid_t) -> CollectionResult<Arc<str>> {\n        if let Some(user) = self.uid_user_mapping.get(&uid) {\n            Ok(user.clone())\n        } else {\n            // SAFETY: getpwuid returns a null pointer if no passwd entry is found for the uid which we check.\n            let passwd = unsafe { libc::getpwuid(uid) };\n\n            if passwd.is_null() {\n                Err(\"passwd is inaccessible\".into())\n            } else {\n                // SAFETY: We return early if passwd is null.\n                let username: Arc<str> = unsafe { std::ffi::CStr::from_ptr((*passwd).pw_name) }\n                    .to_str()\n                    .map_err(|err| CollectionError::General(err.into()))?\n                    .into();\n\n                self.uid_user_mapping.insert(uid, username.clone());\n\n                Ok(username)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/collection/processes/unix.rs",
    "content": "//! Unix-specific parts of process collection.\n\nmod user_table;\n\nuse cfg_if::cfg_if;\npub use user_table::*;\n\ncfg_if! {\n    if #[cfg(all(target_family = \"unix\", not(target_os = \"linux\")))] {\n        mod process_ext;\n        pub(crate) use process_ext::*;\n\n        use super::ProcessHarvest;\n\n        use crate::collection::{DataCollector, processes::*};\n        use crate::collection::error::CollectionResult;\n\n        pub fn sysinfo_process_data(collector: &mut DataCollector) -> CollectionResult<Vec<ProcessHarvest>> {\n            let sys = &collector.sys.system;\n            let use_current_cpu_total = collector.use_current_cpu_total;\n            let unnormalized_cpu = collector.unnormalized_cpu;\n            let total_memory = collector.total_memory();\n            let user_table = &mut collector.user_table;\n\n            cfg_if! {\n                if #[cfg(target_os = \"macos\")] {\n                    MacOSProcessExt::sysinfo_process_data(sys, use_current_cpu_total, unnormalized_cpu, total_memory, user_table)\n                } else if #[cfg(target_os = \"freebsd\")] {\n                    FreeBSDProcessExt::sysinfo_process_data(sys, use_current_cpu_total, unnormalized_cpu, total_memory, user_table)\n                } else {\n                    GenericProcessExt::sysinfo_process_data(sys, use_current_cpu_total, unnormalized_cpu, total_memory, user_table)\n                }\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "src/collection/processes/windows.rs",
    "content": "//! Process data collection for Windows. Uses sysinfo.\n\nuse std::time::Duration;\n\nuse anyhow::bail;\nuse itertools::Itertools;\nuse windows::Win32::{\n    Foundation::{CloseHandle, HANDLE},\n    System::Threading::{GetPriorityClass, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION},\n};\n\nuse super::{ProcessHarvest, process_status_str};\nuse crate::collection::{DataCollector, error::CollectionResult};\n\n/// See [here](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getpriorityclass)\n/// for more information on the core Windows API being called and the meaning of the priorities, as well as the access\n/// rights needed.\nfn get_priority(pid: u32) -> anyhow::Result<i32> {\n    // SAFETY: We check validity of each step and bail on errors. We also close the handle.\n    unsafe {\n        let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid)?;\n        if process_handle.is_invalid() {\n            bail!(\"Failed to open process with PID {pid} to get priority class\");\n        }\n\n        // From docs: \"If the function fails, the return value is zero.\"\n        let priority = GetPriorityClass(process_handle);\n        if priority == 0 {\n            bail!(\"Failed to get priority class for process with PID {pid}\");\n        }\n\n        let handle_result = CloseHandle(process_handle);\n        if let Err(err) = handle_result {\n            bail!(err);\n        }\n\n        Ok(priority as i32)\n    }\n}\n\n// TODO: There's a lot of shared code with this and the unix impl.\npub fn sysinfo_process_data(\n    collector: &mut DataCollector,\n) -> CollectionResult<Vec<ProcessHarvest>> {\n    let sys = &collector.sys.system;\n    let users = &collector.sys.users;\n    let use_current_cpu_total = collector.use_current_cpu_total;\n    let unnormalized_cpu = collector.unnormalized_cpu;\n    let total_memory = collector.total_memory();\n\n    let mut process_vector: Vec<ProcessHarvest> = Vec::new();\n    let process_hashmap = sys.processes();\n    let cpu_usage = sys.global_cpu_usage() / 100.0;\n    let num_processors = sys.cpus().len();\n\n    for process in process_hashmap.values() {\n        let name = if process.name().is_empty() {\n            let process_cmd = process.cmd();\n            if process_cmd.len() > 1 {\n                process_cmd[0].to_string_lossy().to_string()\n            } else {\n                process\n                    .exe()\n                    .and_then(|exe| exe.file_stem())\n                    .and_then(|stem| stem.to_str())\n                    .map(|s| s.to_string())\n                    .unwrap_or(String::new())\n            }\n        } else {\n            process.name().to_string_lossy().to_string()\n        };\n        let command = {\n            let command = process.cmd().iter().map(|s| s.to_string_lossy()).join(\" \");\n            if command.is_empty() {\n                name.clone()\n            } else {\n                command\n            }\n        };\n\n        let process_cpu_usage = {\n            let pcu = {\n                let usage = process.cpu_usage();\n                if unnormalized_cpu || num_processors == 0 {\n                    usage\n                } else {\n                    usage / num_processors as f32\n                }\n            };\n\n            if use_current_cpu_total && cpu_usage > 0.0 {\n                pcu / cpu_usage\n            } else {\n                pcu\n            }\n        };\n\n        let disk_usage = process.disk_usage();\n        let process_state = (process_status_str(process.status()), 'R');\n\n        #[cfg(feature = \"gpu\")]\n        let (gpu_mem, gpu_util, gpu_mem_percent) = {\n            let mut gpu_mem = 0;\n            let mut gpu_util = 0;\n            let mut gpu_mem_percent = 0.0;\n            if let Some(gpus) = &collector.gpu_pids {\n                use crate::collection::processes::Pid;\n\n                gpus.iter().for_each(|gpu| {\n                    // add mem/util for all gpus to pid\n                    if let Some((mem, util)) = gpu.get(&(process.pid().as_u32() as Pid)) {\n                        gpu_mem += mem;\n                        gpu_util += util;\n                    }\n                });\n            }\n            if let Some(gpu_total_mem) = &collector.gpus_total_mem {\n                gpu_mem_percent = (gpu_mem as f64 / *gpu_total_mem as f64 * 100.0) as f32;\n            }\n            (gpu_mem, gpu_util, gpu_mem_percent)\n        };\n\n        let pid = process.pid().as_u32();\n        let priority = get_priority(pid).unwrap_or(0);\n\n        process_vector.push(ProcessHarvest {\n            pid: pid as _,\n            parent_pid: process.parent().map(|p| p.as_u32() as _),\n            name,\n            command,\n            mem_usage_percent: if total_memory > 0 {\n                process.memory() as f64 * 100.0 / total_memory as f64\n            } else {\n                0.0\n            } as f32,\n            mem_usage: process.memory(),\n            virtual_mem: process.virtual_memory(),\n            cpu_usage_percent: process_cpu_usage,\n            read_per_sec: disk_usage.read_bytes,\n            write_per_sec: disk_usage.written_bytes,\n            total_read: disk_usage.total_read_bytes,\n            total_write: disk_usage.total_written_bytes,\n            process_state,\n            user: process\n                .user_id()\n                .and_then(|uid| users.get_user_by_id(uid).map(|user| user.name().into())),\n            time: if process.start_time() == 0 {\n                // Workaround for sysinfo occasionally returning a start time equal to UNIX\n                // epoch, giving a run time in the range of 50+ years. We just\n                // return a time of zero in this case for simplicity.\n                //\n                // TODO: Maybe return an option instead?\n                Duration::ZERO\n            } else {\n                Duration::from_secs(process.run_time())\n            },\n            #[cfg(feature = \"gpu\")]\n            gpu_mem,\n            #[cfg(feature = \"gpu\")]\n            gpu_util,\n            #[cfg(feature = \"gpu\")]\n            gpu_mem_percent,\n            priority, // TODO: Translate this to Windows priority names?\n        });\n    }\n\n    Ok(process_vector)\n}\n"
  },
  {
    "path": "src/collection/processes.rs",
    "content": "//! Data collection for processes.\n//!\n//! For Linux, this is handled by a custom set of functions.\n//! For Windows, macOS, FreeBSD, Android, and Linux, this is handled by sysinfo.\n\nuse cfg_if::cfg_if;\nuse sysinfo::ProcessStatus;\n\ncfg_if! {\n    if #[cfg(target_os = \"linux\")] {\n        pub mod linux;\n        pub use self::linux::*;\n    } else if #[cfg(target_os = \"macos\")] {\n        pub mod macos;\n        pub(crate) use self::macos::*;\n    } else if #[cfg(target_os = \"windows\")] {\n        pub mod windows;\n        pub use self::windows::*;\n    } else if #[cfg(target_os = \"freebsd\")] {\n        pub mod freebsd;\n        pub(crate) use self::freebsd::*;\n    } else if #[cfg(unix)] {\n        pub(crate) struct GenericProcessExt;\n        impl UnixProcessExt for GenericProcessExt {}\n    }\n}\n\ncfg_if! {\n    if #[cfg(unix)] {\n        pub mod unix;\n        pub use self::unix::*;\n    }\n}\n\nuse std::{sync::Arc, time::Duration};\n\nuse super::{DataCollector, error::CollectionResult};\n\ncfg_if! {\n    if #[cfg(target_family = \"windows\")] {\n        /// A Windows process ID.\n        pub type Pid = usize;\n    } else if #[cfg(unix)] {\n        /// A UNIX process ID.\n        pub type Pid = libc::pid_t;\n    }\n}\n\npub type Bytes = u64;\n\n#[cfg(target_os = \"linux\")]\n/// The process entry \"type\".\n#[derive(Debug, Clone, Copy, Default)]\npub enum ProcessType {\n    /// A regular user process.\n    #[default]\n    Regular,\n\n    /// A kernel process.\n    Kernel,\n\n    /// A thread spawned by a regular user process.\n    ProcessThread,\n}\n\n#[cfg(target_os = \"linux\")]\nimpl ProcessType {\n    /// Returns `true` if this is a thread.\n    pub fn is_thread(&self) -> bool {\n        matches!(self, Self::ProcessThread)\n    }\n\n    /// Returns `true` if this is a kernel process.\n    pub fn is_kernel(&self) -> bool {\n        matches!(self, Self::Kernel)\n    }\n}\n\n#[derive(Debug, Clone, Default)]\npub struct ProcessHarvest {\n    /// The pid of the process.\n    pub pid: Pid,\n\n    /// The parent PID of the process. A `parent_pid` of 0 is usually the root.\n    pub parent_pid: Option<Pid>,\n\n    /// CPU usage as a percentage.\n    pub cpu_usage_percent: f32,\n\n    /// Memory usage as a percentage.\n    ///\n    /// TODO: Maybe calculate this on usage? Store the total mem along with the vector of results.\n    pub mem_usage_percent: f32,\n\n    /// Memory usage as bytes.\n    pub mem_usage: Bytes,\n\n    /// Virtual memory.\n    pub virtual_mem: Bytes,\n\n    /// The name of the process.\n    pub name: String,\n\n    /// The exact command for the process.\n    pub command: String,\n\n    /// Bytes read per second.\n    pub read_per_sec: Bytes,\n\n    /// Bytes written per second.\n    pub write_per_sec: Bytes,\n\n    /// The total number of bytes read by the process.\n    pub total_read: Bytes,\n\n    /// The total number of bytes written by the process.\n    pub total_write: Bytes,\n\n    /// The current state of the process (e.g. zombie, asleep).\n    pub process_state: (&'static str, char),\n\n    /// Cumulative process uptime.\n    pub time: Duration,\n\n    /// This is the *effective* user ID of the process. This is only used on\n    /// Unix platforms.\n    #[cfg(unix)]\n    pub uid: Option<libc::uid_t>,\n\n    /// This is the process' user.\n    pub user: Option<Arc<str>>,\n\n    /// Gpu memory usage as bytes.\n    #[cfg(feature = \"gpu\")]\n    pub gpu_mem: u64,\n\n    /// Gpu memory usage as percentage.\n    ///\n    /// TODO: Maybe calculate this on usage? Store the total GPU mem along with the vector of results.\n    #[cfg(feature = \"gpu\")]\n    pub gpu_mem_percent: f32,\n\n    /// Gpu utilization as a percentage.\n    #[cfg(feature = \"gpu\")]\n    pub gpu_util: u32,\n\n    /// The process entry \"type\".\n    #[cfg(target_os = \"linux\")]\n    pub process_type: ProcessType,\n\n    /// The nice value (user-settable scheduling hint).\n    #[cfg(unix)]\n    pub nice: i32,\n\n    /// The kernel scheduling priority.\n    pub priority: i32,\n    // TODO: Additional fields\n    // pub rss_kb: u64,\n    // pub virt_kb: u64,\n}\n\nimpl DataCollector {\n    pub(crate) fn get_processes(&mut self) -> CollectionResult<Vec<ProcessHarvest>> {\n        cfg_if! {\n            if #[cfg(target_os = \"linux\")] {\n                let time_diff = self.data.collection_time\n                    .duration_since(self.last_collection_time)\n                    .as_secs();\n\n                linux_process_data(\n                    self,\n                    time_diff,\n                )\n            } else if #[cfg(any(target_os = \"freebsd\", target_os = \"macos\", target_os = \"windows\", target_os = \"android\", target_os = \"ios\"))] {\n                sysinfo_process_data(self)\n            } else {\n                Err(crate::collection::error::CollectionError::Unsupported)\n            }\n        }\n    }\n}\n\n/// Pulled from [`ProcessStatus::to_string`] to avoid an alloc.\npub(super) fn process_status_str(status: ProcessStatus) -> &'static str {\n    cfg_if::cfg_if! {\n        if #[cfg(target_os = \"linux\")] {\n            match status {\n                ProcessStatus::Idle => \"Idle\",\n                ProcessStatus::Run => \"Runnable\",\n                ProcessStatus::Sleep => \"Sleeping\",\n                ProcessStatus::Stop => \"Stopped\",\n                ProcessStatus::Zombie => \"Zombie\",\n                ProcessStatus::Tracing => \"Tracing\",\n                ProcessStatus::Dead => \"Dead\",\n                ProcessStatus::Wakekill => \"Wakekill\",\n                ProcessStatus::Waking => \"Waking\",\n                ProcessStatus::Parked => \"Parked\",\n                ProcessStatus::UninterruptibleDiskSleep => \"UninterruptibleDiskSleep\",\n                _ => \"Unknown\",\n            }\n        } else if #[cfg(target_os = \"windows\")] {\n            match status {\n                ProcessStatus::Run => \"Runnable\",\n                _ => \"Unknown\",\n            }\n        } else if #[cfg(target_os = \"macos\")] {\n            match status {\n                ProcessStatus::Idle => \"Idle\",\n                ProcessStatus::Run => \"Runnable\",\n                ProcessStatus::Sleep => \"Sleeping\",\n                ProcessStatus::Stop => \"Stopped\",\n                ProcessStatus::Zombie => \"Zombie\",\n                _ => \"Unknown\",\n            }\n        } else if #[cfg(target_os = \"freebsd\")] {\n            match status {\n                ProcessStatus::Idle => \"Idle\",\n                ProcessStatus::Run => \"Runnable\",\n                ProcessStatus::Sleep => \"Sleeping\",\n                ProcessStatus::Stop => \"Stopped\",\n                ProcessStatus::Zombie => \"Zombie\",\n                ProcessStatus::Dead => \"Dead\",\n                ProcessStatus::LockBlocked => \"LockBlocked\",\n                _ => \"Unknown\",\n            }\n        } else {\n            \"Unknown\"\n        }\n    }\n}\n"
  },
  {
    "path": "src/collection/temperature/linux.rs",
    "content": "//! Gets temperature sensor data for Linux platforms.\n\nuse std::{\n    fs,\n    path::{Path, PathBuf},\n};\n\nuse anyhow::Result;\nuse rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};\n\nuse super::TempSensorData;\n#[cfg(feature = \"gpu\")]\nuse crate::collection::amd::get_amd_name;\nuse crate::{app::filter::Filter, collection::linux::utils::is_device_awake};\n\nconst EMPTY_NAME: &str = \"Unknown\";\n\n/// Returned results from grabbing hwmon/coretemp temperature sensor\n/// values or names.\nstruct HwmonResults {\n    temperatures: Vec<TempSensorData>,\n    num_hwmon: usize,\n}\n\n/// Parses and reads temperatures that were in millidegree Celsius, and if\n/// successful, returns a temperature in Celsius.\nfn parse_temp(path: &Path) -> Result<f32> {\n    Ok(fs::read_to_string(path)?.trim_end().parse::<f32>()? / 1_000.0)\n}\n\n/// Get all candidates from hwmon and coretemp. It will also return the number\n/// of entries from hwmon.\nfn get_hwmon_candidates() -> (HashSet<PathBuf>, usize) {\n    let mut dirs = HashSet::default();\n\n    if let Ok(read_dir) = Path::new(\"/sys/class/hwmon\").read_dir() {\n        for entry in read_dir.flatten() {\n            let mut path = entry.path();\n\n            // hwmon includes many sensors, we only want ones with at least one temperature\n            // sensor Reading this file will wake the device, but we're only\n            // checking existence, so it should be fine.\n            if !path.join(\"temp1_input\").exists() {\n                // Note we also check for a `device` subdirectory (e.g.\n                // `/sys/class/hwmon/hwmon*/device/`). This is needed for\n                // CentOS, which adds this extra `/device` directory. See:\n                // - https://github.com/nicolargo/glances/issues/1060\n                // - https://github.com/giampaolo/psutil/issues/971\n                // - https://github.com/giampaolo/psutil/blob/642438375e685403b4cd60b0c0e25b80dd5a813d/psutil/_pslinux.py#L1316\n                //\n                // If it does match, then add the `device/` directory to the path.\n                if path.join(\"device/temp1_input\").exists() {\n                    path.push(\"device\");\n                }\n            }\n\n            dirs.insert(path);\n        }\n    }\n\n    let num_hwmon = dirs.len();\n\n    if let Ok(read_dir) = Path::new(\"/sys/devices/platform\").read_dir() {\n        for entry in read_dir.flatten() {\n            if entry.file_name().to_string_lossy().starts_with(\"coretemp.\") {\n                if let Ok(read_dir) = entry.path().join(\"hwmon\").read_dir() {\n                    for entry in read_dir.flatten() {\n                        let path = entry.path();\n\n                        if path.join(\"temp1_input\").exists() {\n                            // It's possible that there are dupes (represented by symlinks) - the\n                            // easy way is to just substitute the parent\n                            // directory and check if the hwmon\n                            // variant exists already in a set.\n                            //\n                            // For more info, see https://github.com/giampaolo/psutil/pull/1822/files\n                            if let Some(child) = path.file_name() {\n                                let to_check_path = Path::new(\"/sys/class/hwmon\").join(child);\n\n                                if !dirs.contains(&to_check_path) {\n                                    dirs.insert(path);\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    (dirs, num_hwmon)\n}\n\n#[inline]\nfn read_to_string_lossy<P: AsRef<Path>>(path: P) -> Option<String> {\n    fs::read(path)\n        .map(|v| String::from_utf8_lossy(&v).trim().to_string())\n        .ok()\n}\n\n#[inline]\nfn humanize_name(name: String, sensor_name: Option<&String>) -> String {\n    match sensor_name {\n        Some(ty) => format!(\"{name} ({ty})\"),\n        None => name,\n    }\n}\n\n#[inline]\nfn counted_name(seen_names: &mut HashMap<String, u32>, name: String) -> String {\n    if let Some(count) = seen_names.get_mut(&name) {\n        *count += 1;\n        format!(\"{name} ({count})\")\n    } else {\n        seen_names.insert(name.clone(), 0);\n        name\n    }\n}\n\nfn uppercase_first_letter(s: &mut str) {\n    if let Some(r) = s.get_mut(0..1) {\n        r.make_ascii_uppercase();\n    }\n}\n\nfn finalize_name(\n    hwmon_name: Option<String>, sensor_label: Option<String>,\n    fallback_sensor_name: &Option<String>, seen_names: &mut HashMap<String, u32>,\n) -> String {\n    let candidate_name = match (hwmon_name, sensor_label) {\n        (Some(name), Some(mut label)) => match (name.is_empty(), label.is_empty()) {\n            (false, false) => {\n                uppercase_first_letter(&mut label);\n                format!(\"{name}: {label}\")\n            }\n            (true, false) => {\n                uppercase_first_letter(&mut label);\n\n                // We assume label must not be empty.\n                match fallback_sensor_name {\n                    Some(fallback) if !fallback.is_empty() => {\n                        format!(\"{fallback}: {label}\")\n                    }\n                    _ => label,\n                }\n            }\n            (false, true) => name.to_owned(),\n            (true, true) => EMPTY_NAME.to_string(),\n        },\n        (None, Some(mut label)) => match fallback_sensor_name {\n            Some(fallback) if !fallback.is_empty() => {\n                if label.is_empty() {\n                    fallback.to_owned()\n                } else {\n                    uppercase_first_letter(&mut label);\n                    format!(\"{fallback}: {label}\")\n                }\n            }\n            _ => {\n                if label.is_empty() {\n                    EMPTY_NAME.to_string()\n                } else {\n                    uppercase_first_letter(&mut label);\n                    label\n                }\n            }\n        },\n        (Some(name), None) => {\n            if name.is_empty() {\n                EMPTY_NAME.to_string()\n            } else {\n                name\n            }\n        }\n        (None, None) => match fallback_sensor_name {\n            Some(sensor_name) if !sensor_name.is_empty() => sensor_name.to_owned(),\n            _ => EMPTY_NAME.to_string(),\n        },\n    };\n\n    counted_name(seen_names, candidate_name)\n}\n\n/// Get temperature sensors from the linux sysfs interface `/sys/class/hwmon`\n/// and `/sys/devices/platform/coretemp.*`. It returns all found temperature\n/// sensors, and the number of checked hwmon directories (not coretemp\n/// directories).\n///\n/// For more details, see the relevant Linux kernel documentation:\n/// - [`/sys/class/hwmon`](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-hwmon)\n/// - [`/sys/devices/platform/coretemp.*`](https://www.kernel.org/doc/html/v5.14/hwmon/coretemp.html)\n///\n/// This method will return `0` as the temperature for devices, such as GPUs,\n/// that support power management features that have powered themselves off.\n/// Specifically, in laptops with iGPUs and dGPUs, if the dGPU is capable of\n/// entering ACPI D3cold, reading the temperature sensors will wake it,\n/// and keep it awake, wasting power.\n///\n/// For such devices, this method will only query the sensors *only* if\n/// the device is already in ACPI D0. This has the notable issue that\n/// once this happens, the device will be *kept* on through the sensor\n/// reading, and not be able to re-enter ACPI D3cold.\nfn hwmon_temperatures(filter: &Option<Filter>) -> HwmonResults {\n    let mut temperatures: Vec<TempSensorData> = vec![];\n    let mut seen_names: HashMap<String, u32> = HashMap::default();\n\n    let (dirs, num_hwmon) = get_hwmon_candidates();\n\n    // Note that none of this is async if we ever go back to it, but sysfs is in\n    // memory, so in theory none of this should block if we're slightly careful.\n    // Of note is that reading the temperature sensors of a device that has\n    // `/sys/class/hwmon/hwmon*/device/power_state` == `D3cold` will\n    // wake the device up, and will block until it initializes.\n    //\n    // Reading the `hwmon*/device/power_state` or `hwmon*/temp*_label` properties\n    // will not wake the device, and thus not block,\n    // and meaning no sensors have to be hidden depending on `power_state`\n    //\n    // It would probably be more ideal to use a proper async runtime; this would\n    // also allow easy cancellation/timeouts.\n    for file_path in dirs {\n        let sensor_name = read_to_string_lossy(file_path.join(\"name\"));\n        let device = file_path.join(\"device\");\n\n        if !is_device_awake(&device) {\n            let name = finalize_name(None, None, &sensor_name, &mut seen_names);\n            temperatures.push(TempSensorData {\n                name,\n                temperature: None,\n            });\n\n            continue;\n        }\n\n        if let Ok(dir_entries) = file_path.read_dir() {\n            // Enumerate the devices temperature sensors\n            for file in dir_entries.flatten() {\n                let name = file.file_name();\n                let name = name.to_string_lossy();\n\n                // We only want temperature sensors, skip others early\n                if !(name.starts_with(\"temp\") && name.ends_with(\"input\")) {\n                    continue;\n                }\n\n                let temp_path = file.path();\n                let sensor_label_path = file_path.join(name.replace(\"input\", \"label\"));\n                let sensor_label = read_to_string_lossy(sensor_label_path);\n\n                // Do some messing around to get a more sensible name for sensors:\n                // - For GPUs, this will use the kernel device name, ex `card0`\n                // - For nvme drives, this will also use the kernel name, ex `nvme0`. This is\n                //   found differently than for GPUs\n                // - For whatever acpitz is, on my machine this is now `thermal_zone0`.\n                // - For k10temp, this will still be k10temp, but it has to be handled special.\n                let hwmon_name = {\n                    let device = file_path.join(\"device\");\n\n                    // This will exist for GPUs but not others, this is how we find their kernel\n                    // name.\n                    let drm = device.join(\"drm\");\n                    if drm.exists() {\n                        // This should never actually be empty. If it is though, we'll fall back to\n                        // the sensor name later on.\n\n                        #[cfg(feature = \"gpu\")]\n                        {\n                            if let Some(amd_gpu_name) = get_amd_name(&device) {\n                                Some(amd_gpu_name)\n                            } else if let Ok(cards) = drm.read_dir() {\n                                cards.flatten().find_map(|card| {\n                                    card.file_name().to_str().and_then(|name| {\n                                        name.starts_with(\"card\").then(|| {\n                                            humanize_name(\n                                                name.trim().to_string(),\n                                                sensor_name.as_ref(),\n                                            )\n                                        })\n                                    })\n                                })\n                            } else {\n                                None\n                            }\n                        }\n\n                        #[cfg(not(feature = \"gpu\"))]\n                        {\n                            if let Ok(cards) = drm.read_dir() {\n                                cards.flatten().find_map(|card| {\n                                    card.file_name().to_str().and_then(|name| {\n                                        name.starts_with(\"card\").then(|| {\n                                            humanize_name(\n                                                name.trim().to_string(),\n                                                sensor_name.as_ref(),\n                                            )\n                                        })\n                                    })\n                                })\n                            } else {\n                                None\n                            }\n                        }\n                    } else {\n                        // This little mess is to account for stuff like k10temp. This is needed\n                        // because the `device` symlink points to `nvme*`\n                        // for nvme drives, but to PCI buses for anything\n                        // else. If the first character is alphabetic, it's an actual name like\n                        // k10temp or nvme0, not a PCI bus.\n                        fs::read_link(device).ok().and_then(|link| {\n                            let link = link\n                                .file_name()\n                                .and_then(|f| f.to_str())\n                                .map(|s| s.trim().to_owned());\n\n                            match link {\n                                Some(link) if link.as_bytes()[0].is_ascii_alphabetic() => {\n                                    Some(humanize_name(link, sensor_name.as_ref()))\n                                }\n                                _ => None,\n                            }\n                        })\n                    }\n                };\n\n                let name = finalize_name(hwmon_name, sensor_label, &sensor_name, &mut seen_names);\n\n                // TODO: It's possible we may want to move the filter check further up to avoid\n                // probing hwmon if not needed?\n                if Filter::optional_should_keep(filter, &name) {\n                    if let Ok(temp_celsius) = parse_temp(&temp_path) {\n                        temperatures.push(TempSensorData {\n                            name,\n                            temperature: Some(temp_celsius),\n                        });\n                    }\n                }\n            }\n        }\n    }\n\n    HwmonResults {\n        temperatures,\n        num_hwmon,\n    }\n}\n\n/// Gets data from `/sys/class/thermal/thermal_zone*`. This should only be used\n/// if [`hwmon_temperatures`] doesn't return anything to avoid duplicate sensor\n/// results.\n///\n/// See [the Linux kernel documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-thermal)\n/// for more details.\nfn add_thermal_zone_temperatures(temperatures: &mut Vec<TempSensorData>, filter: &Option<Filter>) {\n    let path = Path::new(\"/sys/class/thermal\");\n    let Ok(read_dir) = path.read_dir() else {\n        return;\n    };\n\n    let mut seen_names: HashMap<String, u32> = HashMap::default();\n\n    for entry in read_dir.flatten() {\n        if entry\n            .file_name()\n            .to_string_lossy()\n            .starts_with(\"thermal_zone\")\n        {\n            let file_path = entry.path();\n            let name_path = file_path.join(\"type\");\n\n            if let Some(name) = read_to_string_lossy(name_path) {\n                let name = if name.is_empty() {\n                    EMPTY_NAME.to_string()\n                } else {\n                    name\n                };\n\n                if Filter::optional_should_keep(filter, &name) {\n                    let temp_path = file_path.join(\"temp\");\n                    if let Ok(temp_celsius) = parse_temp(&temp_path) {\n                        let name = counted_name(&mut seen_names, name);\n\n                        temperatures.push(TempSensorData {\n                            name,\n                            temperature: Some(temp_celsius),\n                        });\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Gets temperature sensors and data.\npub fn get_temperature_data(filter: &Option<Filter>) -> Result<Option<Vec<TempSensorData>>> {\n    let mut results = hwmon_temperatures(filter);\n\n    if results.num_hwmon == 0 {\n        add_thermal_zone_temperatures(&mut results.temperatures, filter);\n    }\n\n    Ok(Some(results.temperatures))\n}\n\n#[cfg(test)]\nmod tests {\n    use rustc_hash::FxHashMap as HashMap;\n\n    use super::finalize_name;\n\n    #[test]\n    fn test_finalize_name() {\n        let mut seen_names = HashMap::default();\n\n        assert_eq!(\n            finalize_name(\n                Some(\"hwmon\".to_string()),\n                Some(\"sensor\".to_string()),\n                &Some(\"test\".to_string()),\n                &mut seen_names\n            ),\n            \"hwmon: Sensor\"\n        );\n\n        assert_eq!(\n            finalize_name(\n                Some(\"hwmon\".to_string()),\n                None,\n                &Some(\"test\".to_string()),\n                &mut seen_names\n            ),\n            \"hwmon\"\n        );\n\n        assert_eq!(\n            finalize_name(\n                None,\n                Some(\"sensor\".to_string()),\n                &Some(\"test\".to_string()),\n                &mut seen_names\n            ),\n            \"test: Sensor\"\n        );\n\n        assert_eq!(\n            finalize_name(\n                Some(\"hwmon\".to_string()),\n                Some(\"sensor\".to_string()),\n                &Some(\"test\".to_string()),\n                &mut seen_names\n            ),\n            \"hwmon: Sensor (1)\"\n        );\n\n        assert_eq!(\n            finalize_name(None, None, &Some(\"test\".to_string()), &mut seen_names),\n            \"test\"\n        );\n\n        assert_eq!(finalize_name(None, None, &None, &mut seen_names), \"Unknown\");\n\n        assert_eq!(\n            finalize_name(None, None, &Some(\"test\".to_string()), &mut seen_names),\n            \"test (1)\"\n        );\n\n        assert_eq!(\n            finalize_name(None, None, &None, &mut seen_names),\n            \"Unknown (1)\"\n        );\n\n        assert_eq!(\n            finalize_name(Some(String::default()), None, &None, &mut seen_names),\n            \"Unknown (2)\"\n        );\n\n        assert_eq!(\n            finalize_name(None, Some(String::default()), &None, &mut seen_names),\n            \"Unknown (3)\"\n        );\n\n        assert_eq!(\n            finalize_name(None, None, &Some(String::default()), &mut seen_names),\n            \"Unknown (4)\"\n        );\n    }\n}\n"
  },
  {
    "path": "src/collection/temperature/sysinfo.rs",
    "content": "//! Gets temperature data via sysinfo.\n\nuse anyhow::Result;\n\nuse super::TempSensorData;\nuse crate::app::filter::Filter;\n\npub fn get_temperature_data(\n    components: &sysinfo::Components, filter: &Option<Filter>,\n) -> Result<Option<Vec<TempSensorData>>> {\n    let mut temperatures: Vec<TempSensorData> = Vec::new();\n\n    for component in components {\n        let name = component.label().to_string();\n\n        if Filter::optional_should_keep(filter, &name) {\n            temperatures.push(TempSensorData {\n                name,\n                temperature: component.temperature(),\n            });\n        }\n    }\n\n    // For RockPro64 boards on FreeBSD, they apparently use \"hw.temperature\" for\n    // sensors.\n    #[cfg(target_os = \"freebsd\")]\n    {\n        use sysctl::Sysctl;\n\n        const KEY: &str = \"hw.temperature\";\n        if let Ok(root) = sysctl::Ctl::new(KEY) {\n            for ctl in sysctl::CtlIter::below(root).flatten() {\n                if let (Ok(name), Ok(temp)) = (ctl.name(), ctl.value()) {\n                    if let Some(temp) = temp.as_temperature() {\n                        temperatures.push(TempSensorData {\n                            name,\n                            temperature: Some(temp.celsius()),\n                        });\n                    }\n                }\n            }\n        }\n    }\n\n    // TODO: Should we instead use a hashmap -> vec to skip dupes?\n    Ok(Some(temperatures))\n}\n"
  },
  {
    "path": "src/collection/temperature.rs",
    "content": "//! Data collection for temperature metrics.\n//!\n//! For Linux, this is handled by custom code.\n//! For everything else, this is handled by sysinfo.\n\ncfg_if::cfg_if! {\n    if #[cfg(target_os = \"linux\")] {\n        pub mod linux;\n        pub use self::linux::*;\n    } else {\n        pub mod sysinfo;\n        pub use self::sysinfo::*;\n    }\n}\n\n#[derive(Default, Debug, Clone)]\npub struct TempSensorData {\n    /// The name of the sensor.\n    pub name: String,\n\n    /// The temperature in Celsius.\n    pub temperature: Option<f32>,\n}\n"
  },
  {
    "path": "src/collection.rs",
    "content": "//! This is the main file to house data collection functions.\n//!\n//! TODO: Rename this to intake? Collection?\n\n#[cfg(feature = \"nvidia\")]\npub mod nvidia;\n\n#[cfg(all(target_os = \"linux\", feature = \"gpu\"))]\npub mod amd;\n\n#[cfg(target_os = \"linux\")]\nmod linux {\n    pub mod utils;\n}\n\n#[cfg(feature = \"battery\")]\npub mod batteries;\npub mod cpu;\npub mod disks;\npub mod error;\npub mod memory;\npub mod network;\npub mod processes;\npub mod temperature;\n\nuse std::time::{Duration, Instant};\n\n#[cfg(any(target_os = \"linux\", feature = \"gpu\"))]\nuse nohash::IntMap;\n#[cfg(any(not(target_os = \"windows\"), feature = \"gpu\"))]\nuse processes::Pid;\n#[cfg(feature = \"battery\")]\nuse starship_battery::{Battery, Manager};\n\nuse super::DataFilters;\nuse crate::app::layout_manager::UsedWidgets;\n\n// TODO: We can possibly reuse an internal buffer for this to reduce allocs.\n#[derive(Clone, Debug)]\npub struct Data {\n    pub collection_time: Instant,\n    pub cpu: Option<cpu::CpuHarvest>,\n    pub load_avg: Option<cpu::LoadAvgHarvest>,\n    pub memory: Option<memory::MemData>,\n    #[cfg(not(target_os = \"windows\"))]\n    pub cache: Option<memory::MemData>,\n    pub swap: Option<memory::MemData>,\n    pub temperature_sensors: Option<Vec<temperature::TempSensorData>>,\n    pub network: Option<network::NetworkHarvest>,\n    pub list_of_processes: Option<Vec<processes::ProcessHarvest>>,\n    pub disks: Option<Vec<disks::DiskHarvest>>,\n    pub io: Option<disks::IoHarvest>,\n    #[cfg(feature = \"battery\")]\n    pub list_of_batteries: Option<Vec<batteries::BatteryData>>,\n    #[cfg(feature = \"zfs\")]\n    pub arc: Option<memory::MemData>,\n    #[cfg(feature = \"gpu\")]\n    pub gpu: Option<Vec<(String, memory::MemData)>>,\n}\n\nimpl Default for Data {\n    fn default() -> Self {\n        Data {\n            collection_time: Instant::now(),\n            cpu: None,\n            load_avg: None,\n            memory: None,\n            #[cfg(not(target_os = \"windows\"))]\n            cache: None,\n            swap: None,\n            temperature_sensors: None,\n            list_of_processes: None,\n            disks: None,\n            io: None,\n            network: None,\n            #[cfg(feature = \"battery\")]\n            list_of_batteries: None,\n            #[cfg(feature = \"zfs\")]\n            arc: None,\n            #[cfg(feature = \"gpu\")]\n            gpu: None,\n        }\n    }\n}\n\nimpl Data {\n    pub fn cleanup(&mut self) {\n        self.io = None;\n        self.temperature_sensors = None;\n        self.list_of_processes = None;\n        self.disks = None;\n        self.memory = None;\n        self.swap = None;\n        self.cpu = None;\n        self.load_avg = None;\n\n        if let Some(network) = &mut self.network {\n            network.first_run_cleanup();\n        }\n        #[cfg(feature = \"zfs\")]\n        {\n            self.arc = None;\n        }\n        #[cfg(feature = \"gpu\")]\n        {\n            self.gpu = None;\n        }\n    }\n}\n\n/// A wrapper around the sysinfo data source. We use sysinfo for the following\n/// data:\n/// - CPU usage\n/// - Memory usage\n/// - Network usage\n/// - Processes (non-Linux)\n/// - Disk (anything outside of Linux, macOS, and FreeBSD)\n/// - Temperatures (non-Linux)\n#[derive(Debug)]\npub struct SysinfoSource {\n    /// Handles CPU, memory, and processes.\n    pub(crate) system: sysinfo::System,\n    pub(crate) network: sysinfo::Networks,\n    #[cfg(not(target_os = \"linux\"))]\n    pub(crate) temps: sysinfo::Components,\n    #[cfg(not(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\")))]\n    pub(crate) disks: sysinfo::Disks,\n    #[cfg(target_os = \"windows\")]\n    pub(crate) users: sysinfo::Users,\n}\n\nimpl Default for SysinfoSource {\n    fn default() -> Self {\n        use sysinfo::*;\n\n        Self {\n            system: System::new(),\n            network: Networks::new(),\n            #[cfg(not(target_os = \"linux\"))]\n            temps: Components::new(),\n            #[cfg(not(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\")))]\n            disks: Disks::new(),\n            #[cfg(target_os = \"windows\")]\n            users: Users::new(),\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct DataCollector {\n    pub data: Data,\n    sys: SysinfoSource,\n    last_collection_time: Instant,\n    widgets_to_harvest: UsedWidgets,\n    filters: DataFilters,\n\n    total_rx: u64,\n    total_tx: u64,\n    total_rx_packets: u64,\n    total_tx_packets: u64,\n\n    unnormalized_cpu: bool,\n    use_current_cpu_total: bool,\n    show_average_cpu: bool,\n    get_process_threads: bool,\n\n    last_list_collection_time: Instant,\n    should_run_less_routine_tasks: bool,\n\n    #[cfg(target_os = \"linux\")]\n    prev_process_details: IntMap<Pid, processes::PrevProcDetails>,\n    #[cfg(target_os = \"linux\")]\n    prev_idle: f64,\n    #[cfg(target_os = \"linux\")]\n    prev_non_idle: f64,\n\n    #[cfg(feature = \"battery\")]\n    battery_manager: Option<Manager>,\n    #[cfg(feature = \"battery\")]\n    battery_list: Option<Vec<Battery>>,\n\n    #[cfg(unix)]\n    user_table: processes::UserTable,\n\n    #[cfg(feature = \"gpu\")]\n    gpu_pids: Option<Vec<IntMap<Pid, (u64, u32)>>>,\n    #[cfg(feature = \"gpu\")]\n    gpus_total_mem: Option<u64>,\n    #[cfg(feature = \"zfs\")]\n    free_arc_mem: bool,\n}\n\nconst LESS_ROUTINE_TASK_TIME: Duration = Duration::from_secs(60);\n\nimpl DataCollector {\n    pub fn new(filters: DataFilters) -> Self {\n        // Initialize it to the past to force it to load on initialization.\n        let now = Instant::now();\n        let last_collection_time = now.checked_sub(LESS_ROUTINE_TASK_TIME * 10).unwrap_or(now);\n\n        DataCollector {\n            data: Data::default(),\n            sys: SysinfoSource::default(),\n            #[cfg(target_os = \"linux\")]\n            prev_process_details: IntMap::default(),\n            #[cfg(target_os = \"linux\")]\n            prev_idle: 0_f64,\n            #[cfg(target_os = \"linux\")]\n            prev_non_idle: 0_f64,\n            use_current_cpu_total: false,\n            unnormalized_cpu: false,\n            get_process_threads: false,\n            last_collection_time,\n            total_rx: 0,\n            total_tx: 0,\n            total_rx_packets: 0,\n            total_tx_packets: 0,\n            show_average_cpu: false,\n            widgets_to_harvest: UsedWidgets::default(),\n            #[cfg(feature = \"battery\")]\n            battery_manager: None,\n            #[cfg(feature = \"battery\")]\n            battery_list: None,\n            filters,\n            #[cfg(unix)]\n            user_table: Default::default(),\n            #[cfg(feature = \"gpu\")]\n            gpu_pids: None,\n            #[cfg(feature = \"gpu\")]\n            gpus_total_mem: None,\n            #[cfg(feature = \"zfs\")]\n            free_arc_mem: false,\n            last_list_collection_time: last_collection_time,\n            should_run_less_routine_tasks: true,\n        }\n    }\n\n    /// Update the check for routine tasks like updating lists of batteries, cleanup, etc.\n    /// This is useful for things that we don't want to update all the time.\n    ///\n    /// Note this should be set back to false if `self.last_list_collection_time` is updated.\n    #[inline]\n    fn run_less_routine_tasks(&mut self) {\n        if self\n            .data\n            .collection_time\n            .duration_since(self.last_list_collection_time)\n            > LESS_ROUTINE_TASK_TIME\n        {\n            self.should_run_less_routine_tasks = true;\n        }\n\n        if self.should_run_less_routine_tasks {\n            self.last_list_collection_time = self.data.collection_time;\n        }\n    }\n\n    pub fn set_collection(&mut self, used_widgets: UsedWidgets) {\n        self.widgets_to_harvest = used_widgets;\n    }\n\n    pub fn set_use_current_cpu_total(&mut self, use_current_cpu_total: bool) {\n        self.use_current_cpu_total = use_current_cpu_total;\n    }\n\n    pub fn set_unnormalized_cpu(&mut self, unnormalized_cpu: bool) {\n        self.unnormalized_cpu = unnormalized_cpu;\n    }\n\n    pub fn set_show_average_cpu(&mut self, show_average_cpu: bool) {\n        self.show_average_cpu = show_average_cpu;\n    }\n\n    pub fn set_get_process_threads(&mut self, get_process_threads: bool) {\n        self.get_process_threads = get_process_threads;\n    }\n\n    #[cfg(feature = \"zfs\")]\n    pub fn set_free_arc_mem(&mut self, free_mem: bool) {\n        self.free_arc_mem = free_mem;\n    }\n\n    /// Refresh sysinfo data. We use sysinfo for the following data:\n    /// - CPU usage\n    /// - Memory usage\n    /// - Network usage\n    /// - Processes (non-Linux)\n    /// - Disk (Windows)\n    /// - Temperatures (non-Linux)\n    fn refresh_sysinfo_data(&mut self) {\n        // Refresh the list of objects once every minute. If it's too frequent it can\n        // cause segfaults.\n\n        if self.widgets_to_harvest.use_cpu || self.widgets_to_harvest.use_proc {\n            self.sys.system.refresh_cpu_all();\n        }\n\n        if self.widgets_to_harvest.use_mem || self.widgets_to_harvest.use_proc {\n            self.sys.system.refresh_memory();\n        }\n\n        if self.widgets_to_harvest.use_net {\n            self.sys.network.refresh(true);\n        }\n\n        // sysinfo is used on non-Linux systems for the following:\n        // - Processes (users list as well for Windows)\n        // - Disks (Windows only)\n        // - Temperatures and temperature components list.\n        #[cfg(not(target_os = \"linux\"))]\n        {\n            if self.widgets_to_harvest.use_proc {\n                self.sys.system.refresh_processes_specifics(\n                    sysinfo::ProcessesToUpdate::All,\n                    true,\n                    sysinfo::ProcessRefreshKind::everything()\n                        .without_environ()\n                        .without_cwd()\n                        .without_root(),\n                );\n\n                // For Windows, sysinfo also handles the users list.\n                #[cfg(target_os = \"windows\")]\n                if self.should_run_less_routine_tasks {\n                    self.sys.users.refresh();\n                }\n            }\n\n            if self.widgets_to_harvest.use_temp {\n                if self.should_run_less_routine_tasks {\n                    self.sys.temps.refresh(true);\n                }\n\n                for component in self.sys.temps.iter_mut() {\n                    component.refresh();\n                }\n            }\n\n            #[cfg(target_os = \"windows\")]\n            if self.widgets_to_harvest.use_disk {\n                if self.should_run_less_routine_tasks {\n                    self.sys.disks.refresh(true);\n                }\n\n                for disk in self.sys.disks.iter_mut() {\n                    disk.refresh();\n                }\n            }\n        }\n    }\n\n    /// Update and refresh data.\n    ///\n    /// TODO: separate refresh steps and update steps\n    pub fn update_data(&mut self) {\n        self.data.collection_time = Instant::now();\n\n        self.run_less_routine_tasks();\n\n        self.refresh_sysinfo_data();\n\n        self.update_cpu_usage();\n        self.update_memory_usage();\n        self.update_temps();\n\n        #[cfg(feature = \"battery\")]\n        self.update_batteries();\n\n        #[cfg(feature = \"gpu\")]\n        self.update_gpus();\n\n        self.update_processes();\n        self.update_network_usage();\n        self.update_disks();\n\n        // Make sure to run this to refresh the setting.\n        self.should_run_less_routine_tasks = false;\n\n        // Update times for future reference.\n        self.last_collection_time = self.data.collection_time;\n    }\n\n    /// Gets GPU data. Note this will usually append to other previously\n    /// collected data fields at the moment.\n    #[cfg(feature = \"gpu\")]\n    #[inline]\n    fn update_gpus(&mut self) {\n        if self.widgets_to_harvest.use_gpu {\n            let mut local_gpu: Vec<(String, memory::MemData)> = Vec::new();\n            let mut local_gpu_pids: Vec<IntMap<Pid, (u64, u32)>> = Vec::new();\n            let mut local_gpu_total_mem: u64 = 0;\n\n            #[cfg(feature = \"nvidia\")]\n            if let Some(data) =\n                nvidia::get_nvidia_vecs(&self.filters.temp_filter, &self.widgets_to_harvest)\n            {\n                if let Some(mut temp) = data.temperature {\n                    if let Some(sensors) = &mut self.data.temperature_sensors {\n                        sensors.append(&mut temp);\n                    } else {\n                        self.data.temperature_sensors = Some(temp);\n                    }\n                }\n                if let Some(mut mem) = data.memory {\n                    local_gpu.append(&mut mem);\n                }\n                if let Some(mut proc) = data.procs {\n                    local_gpu_pids.append(&mut proc.1);\n                    local_gpu_total_mem += proc.0;\n                }\n            }\n\n            #[cfg(target_os = \"linux\")]\n            if let Some(data) =\n                amd::get_amd_vecs(&self.widgets_to_harvest, self.last_collection_time)\n            {\n                if let Some(mut mem) = data.memory {\n                    local_gpu.append(&mut mem);\n                }\n                if let Some(mut proc) = data.procs {\n                    local_gpu_pids.append(&mut proc.1);\n                    local_gpu_total_mem += proc.0;\n                }\n            }\n\n            self.data.gpu = (!local_gpu.is_empty()).then_some(local_gpu);\n            self.gpu_pids = (!local_gpu_pids.is_empty()).then_some(local_gpu_pids);\n            self.gpus_total_mem = (local_gpu_total_mem > 0).then_some(local_gpu_total_mem);\n        }\n    }\n\n    #[inline]\n    fn update_cpu_usage(&mut self) {\n        if self.widgets_to_harvest.use_cpu {\n            self.data.cpu = cpu::get_cpu_data_list(&self.sys.system, self.show_average_cpu).ok();\n\n            #[cfg(unix)]\n            {\n                self.data.load_avg = Some(cpu::get_load_avg());\n            }\n        }\n    }\n\n    #[inline]\n    fn update_processes(&mut self) {\n        if self.widgets_to_harvest.use_proc {\n            if let Ok(mut process_list) = self.get_processes() {\n                // NB: To avoid duplicate sorts on rerenders/events, we sort the processes by\n                // PID here. We also want to avoid re-sorting *again* later on\n                // if we're sorting by PID, since we already did it here!\n                process_list.sort_unstable_by_key(|p| p.pid);\n                self.data.list_of_processes = Some(process_list);\n            }\n        }\n    }\n\n    #[inline]\n    fn update_temps(&mut self) {\n        if self.widgets_to_harvest.use_temp {\n            #[cfg(not(target_os = \"linux\"))]\n            if let Ok(data) =\n                temperature::get_temperature_data(&self.sys.temps, &self.filters.temp_filter)\n            {\n                self.data.temperature_sensors = data;\n            }\n\n            #[cfg(target_os = \"linux\")]\n            if let Ok(data) = temperature::get_temperature_data(&self.filters.temp_filter) {\n                self.data.temperature_sensors = data;\n            }\n        }\n    }\n\n    #[inline]\n    fn update_memory_usage(&mut self) {\n        if self.widgets_to_harvest.use_mem {\n            self.data.memory = memory::get_ram_usage(&self.sys.system);\n\n            #[cfg(feature = \"zfs\")]\n            {\n                #[cfg(any(target_os = \"linux\", target_os = \"freebsd\"))]\n                if let Some(arc) = memory::arc::get_arc_usage() {\n                    if let Some(mem) = &mut self.data.memory {\n                        if self.free_arc_mem {\n                            if arc.0.used_bytes > arc.1 {\n                                #[cfg(target_os = \"linux\")]\n                                {\n                                    mem.used_bytes -= arc.0.used_bytes.saturating_sub(arc.1); // keep arc min like htop\n                                }\n                                #[cfg(target_os = \"freebsd\")]\n                                {\n                                    mem.used_bytes += arc.1; // sysinfo subtracts arc_size on freebsd\n                                }\n                            } else {\n                                #[cfg(target_os = \"freebsd\")]\n                                {\n                                    mem.used_bytes += arc.0.used_bytes;\n                                }\n                            }\n                        } else {\n                            #[cfg(target_os = \"freebsd\")]\n                            {\n                                mem.used_bytes += arc.0.used_bytes;\n                            }\n                        }\n                    }\n\n                    self.data.arc = Some(arc.0);\n                }\n            }\n\n            #[cfg(not(target_os = \"windows\"))]\n            if self.widgets_to_harvest.use_cache {\n                self.data.cache = memory::get_cache_usage(&self.sys.system);\n            }\n\n            self.data.swap = memory::get_swap_usage(&self.sys.system);\n        }\n    }\n\n    #[inline]\n    fn update_network_usage(&mut self) {\n        if self.widgets_to_harvest.use_net {\n            let net_data = network::get_network_data(\n                &self.sys.network,\n                self.last_collection_time,\n                &mut self.total_rx,\n                &mut self.total_tx,\n                &mut self.total_rx_packets,\n                &mut self.total_tx_packets,\n                self.data.collection_time,\n                &self.filters.net_filter,\n            );\n\n            self.total_rx = net_data.total_rx;\n            self.total_tx = net_data.total_tx;\n            self.total_rx_packets = net_data.total_rx_packets;\n            self.total_tx_packets = net_data.total_tx_packets;\n            self.data.network = Some(net_data);\n        }\n    }\n\n    /// Update battery information.\n    ///\n    /// If the battery manager is not initialized, it will attempt to initialize it if at least one battery is found.\n    ///\n    /// This function also refreshes the list of batteries if `self.should_run_less_routine_tasks` is true.\n    #[inline]\n    #[cfg(feature = \"battery\")]\n    fn update_batteries(&mut self) {\n        let battery_manager = match &self.battery_manager {\n            Some(manager) => {\n                // Also check if we need to refresh the list of batteries.\n                if self.should_run_less_routine_tasks {\n                    let battery_list = manager\n                        .batteries()\n                        .map(|batteries| batteries.filter_map(Result::ok).collect::<Vec<_>>());\n\n                    if let Ok(battery_list) = battery_list {\n                        if battery_list.is_empty() {\n                            self.battery_list = None;\n                        } else {\n                            self.battery_list = Some(battery_list);\n                        }\n                    } else {\n                        self.battery_list = None;\n                    }\n                }\n\n                manager\n            }\n            None => {\n                if let Ok(manager) = Manager::new() {\n                    let Ok(batteries) = manager.batteries() else {\n                        return;\n                    };\n\n                    let battery_list = batteries.filter_map(Result::ok).collect::<Vec<_>>();\n\n                    if battery_list.is_empty() {\n                        return;\n                    }\n\n                    self.battery_list = Some(battery_list);\n                    self.battery_manager.insert(manager)\n                } else {\n                    return;\n                }\n            }\n        };\n\n        self.data.list_of_batteries = self\n            .battery_list\n            .as_mut()\n            .map(|battery_list| batteries::refresh_batteries(battery_manager, battery_list));\n    }\n\n    #[inline]\n    fn update_disks(&mut self) {\n        if self.widgets_to_harvest.use_disk {\n            self.data.disks = disks::get_disk_usage(self).ok();\n            self.data.io = disks::get_io_usage().ok();\n        }\n    }\n\n    /// Returns the total memory of the system.\n    #[inline]\n    fn total_memory(&self) -> u64 {\n        if let Some(memory) = &self.data.memory {\n            memory.total_bytes.get()\n        } else {\n            self.sys.system.total_memory()\n        }\n    }\n}\n\n#[cfg(target_os = \"freebsd\")]\n/// Deserialize [libxo](https://www.freebsd.org/cgi/man.cgi?query=libxo&apropos=0&sektion=0&manpath=FreeBSD+13.1-RELEASE+and+Ports&arch=default&format=html) JSON data\nfn deserialize_xo<T>(key: &str, data: &[u8]) -> Result<T, std::io::Error>\nwhere\n    T: serde::de::DeserializeOwned,\n{\n    let mut value: serde_json::Value = serde_json::from_slice(data)?;\n    value\n        .as_object_mut()\n        .and_then(|map| map.remove(key))\n        .ok_or_else(|| std::io::Error::other(\"key not found\"))\n        .and_then(|val| serde_json::from_value(val).map_err(|err| err.into()))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_data_collection() {\n        let mut collector = DataCollector::new(DataFilters {\n            disk_filter: None,\n            mount_filter: None,\n            temp_filter: None,\n            net_filter: None,\n        });\n\n        // #[cfg(feature = \"battery\")]\n        // {\n        //     collector.widgets_to_harvest.use_battery = true;\n        // }\n\n        // #[cfg(feature = \"zfs\")]\n        // {\n        //     collector.widgets_to_harvest.use_cache = true;\n        // }\n\n        // #[cfg(feature = \"gpu\")]\n        // {\n        //     collector.widgets_to_harvest.use_gpu = true;\n        // }\n\n        collector.widgets_to_harvest.use_cpu = true;\n        collector.widgets_to_harvest.use_disk = true;\n        collector.widgets_to_harvest.use_mem = true;\n        collector.widgets_to_harvest.use_net = true;\n        collector.widgets_to_harvest.use_proc = true;\n        collector.widgets_to_harvest.use_temp = true;\n\n        collector.update_data();\n\n        let data = collector.data;\n\n        assert!(!data.cpu.unwrap().is_empty());\n        assert!(!data.disks.unwrap().is_empty());\n        assert!(data.memory.is_some());\n        assert!(data.network.is_some());\n        assert!(!data.list_of_processes.unwrap().is_empty());\n        assert!(data.temperature_sensors.is_some());\n    }\n}\n"
  },
  {
    "path": "src/constants.rs",
    "content": "//! A bunch of constants used throughout the application.\n//!\n//! FIXME: Move these to where it makes more sense.\n\n// Default widget ID\npub const DEFAULT_WIDGET_ID: u64 = 56709;\n\n// Limits for when we should stop showing table gaps/labels (anything less means\n// not shown)\npub const TABLE_GAP_HEIGHT_LIMIT: u16 = 7;\n\n// Help text\nconst HELP_CONTENTS_TEXT: [&str; 10] = [\n    \"Either scroll or press the number key to go to the corresponding help menu section:\",\n    \"1 - General\",\n    \"2 - CPU widget\",\n    \"3 - Process widget\",\n    \"4 - Process search widget\",\n    \"5 - Process sort widget\",\n    \"6 - Temperature widget\",\n    \"7 - Disk widget\",\n    \"8 - Battery widget\",\n    \"9 - Basic memory widget\",\n];\n\n// TODO [Help]: Search in help?\n// TODO [Help]: Move to using tables for easier formatting?\npub(crate) const GENERAL_HELP_TEXT: [&str; 28] = [\n    \"1 - General\",\n    \"q, Ctrl-c            Quit\",\n    \"Esc                  Close dialog windows, search, widgets, or exit expanded mode\",\n    \"Ctrl-r               Reset display and any collected data\",\n    \"f                    Freeze/unfreeze updating with new data\",\n    \"Ctrl-Left,           \",\n    \"Shift-Left, H, A     Move widget selection left\",\n    \"Ctrl-Right,          \",\n    \"Shift-Right, L, D    Move widget selection right\",\n    \"Ctrl-Up,             \",\n    \"Shift-Up, K, W       Move widget selection up\",\n    \"Ctrl-Down,           \",\n    \"Shift-Down, J, S     Move widget selection down\",\n    \"Left, h              Move left within widget\",\n    \"Down, j              Move down within widget\",\n    \"Up, k                Move up within widget\",\n    \"Right, l             Move right within widget\",\n    \"?                    Open help menu\",\n    \"gg                   Jump to the first entry\",\n    \"G                    Jump to the last entry\",\n    \"e                    Toggle expanding the currently selected widget\",\n    \"+                    Zoom in on chart (decrease time range)\",\n    \"-                    Zoom out on chart (increase time range)\",\n    \"=                    Reset zoom\",\n    \"PgUp, PgDown         Scroll up/down a table by a page\",\n    \"Ctrl-u, Ctrl-d       Scroll up/down a table by half a page\",\n    \"Mouse scroll         Scroll through the tables or zoom in/out of charts by scrolling up/down\",\n    \"Mouse click          Selects the clicked widget, table entry, dialog option, or tab\",\n];\n\nconst CPU_HELP_TEXT: [&str; 2] = [\n    \"2 - CPU widget\",\n    \"Mouse scroll         Scrolling over a CPU core/average shows only that entry on the chart\",\n];\n\nconst PROCESS_HELP_TEXT: [&str; 20] = [\n    \"3 - Process widget\",\n    \"dd, F9, Delete          Kill the selected process\",\n    \"c                       Sort by CPU usage, press again to reverse\",\n    \"m                       Sort by memory usage, press again to reverse\",\n    \"p                       Sort by PID name, press again to reverse\",\n    \"n                       Sort by process name, press again to reverse\",\n    \"Tab                     Group/un-group processes with the same name\",\n    \"Ctrl-f, /               Open process search widget\",\n    \"P                       Toggle between showing the full command or just the process name\",\n    \"s, F6                   Open process sort widget\",\n    \"I                       Invert current sort\",\n    \"%                       Toggle between values and percentages for memory usage\",\n    \"t, F5                   Toggle tree mode\",\n    \"Right                   Collapse a branch while in tree mode\",\n    \"Left                    Expand a branch while in tree mode\",\n    \"+, -, click, Space      Toggle whether a branch is expanded or collapsed in tree mode\",\n    \"click on header         Sorts the entries by that column, click again to invert the sort\",\n    \"C                       Sort by GPU usage, press again to reverse\",\n    \"M                       Sort by GPU memory usage, press again to reverse\",\n    \"z                       Toggle the display of kernel threads\",\n];\n\nconst SEARCH_HELP_TEXT: [&str; 51] = [\n    \"4 - Process search widget\",\n    \"Esc                  Close the search widget (retains the filter)\",\n    \"Ctrl-a               Skip to the start of the search query\",\n    \"Ctrl-e               Skip to the end of the search query\",\n    \"Ctrl-u               Clear the current search query\",\n    \"Ctrl-w               Delete a word behind the cursor\",\n    \"Ctrl-h               Delete the character behind the cursor\",\n    \"Backspace            Delete the character behind the cursor\",\n    \"Delete               Delete the character at the cursor\",\n    \"Alt-c, F1            Toggle matching case\",\n    \"Alt-w, F2            Toggle matching the entire word\",\n    \"Alt-r, F3            Toggle using regex\",\n    \"Left, Alt-h          Move cursor left\",\n    \"Right, Alt-l         Move cursor right\",\n    \"\",\n    \"Supported search types:\",\n    \"<by name/cmd>        ex: btm\",\n    \"pid                  ex: pid 825\",\n    \"cpu, cpu%            ex: cpu > 4.2\",\n    \"mem, mem%            ex: mem < 4.2\",\n    \"memb                 ex: memb < 100 kb\",\n    \"read, r/s, rps       ex: read >= 1 b\",\n    \"write, w/s, wps      ex: write <= 1 tb\",\n    \"tread, t.read        ex: tread = 1\",\n    \"twrite, t.write      ex: twrite = 1\",\n    \"user                 ex: user = root\",\n    \"state                ex: state = running\",\n    \"gpu%                 ex: gpu% < 4.2\",\n    \"gmem                 ex: gmem < 100 kb\",\n    \"gmem%                ex: gmem% < 4.2\",\n    \"\",\n    \"Comparison operators:\",\n    \"=                    ex: cpu = 1\",\n    \">                    ex: cpu > 1\",\n    \"<                    ex: cpu < 1\",\n    \">=                   ex: cpu >= 1\",\n    \"<=                   ex: cpu <= 1\",\n    \"\",\n    \"Logical operators:\",\n    \"and, &&, <Space>     ex: btm and cpu > 1 and mem > 1\",\n    \"or, ||               ex: btm or firefox\",\n    \"\",\n    \"Supported units:\",\n    \"B                    ex: read > 1 b\",\n    \"KB                   ex: read > 1 kb\",\n    \"MB                   ex: read > 1 mb\",\n    \"TB                   ex: read > 1 tb\",\n    \"KiB                  ex: read > 1 kib\",\n    \"MiB                  ex: read > 1 mib\",\n    \"GiB                  ex: read > 1 gib\",\n    \"TiB                  ex: read > 1 tib\",\n];\n\nconst SORT_HELP_TEXT: [&str; 6] = [\n    \"5 - Sort widget\",\n    \"Down, 'j'            Scroll down in list\",\n    \"Up, 'k'              Scroll up in list\",\n    \"Mouse scroll         Scroll through sort widget\",\n    \"Esc                  Close the sort widget\",\n    \"Enter                Sort by current selected column\",\n];\n\nconst TEMP_HELP_WIDGET: [&str; 3] = [\n    \"6 - Temperature widget\",\n    \"'s'                  Sort by sensor name, press again to reverse\",\n    \"'t'                  Sort by temperature, press again to reverse\",\n];\n\nconst DISK_HELP_WIDGET: [&str; 9] = [\n    \"7 - Disk widget\",\n    \"'d'                  Sort by disk name, press again to reverse\",\n    \"'m'                  Sort by disk mount, press again to reverse\",\n    \"'u'                  Sort by disk usage, press again to reverse\",\n    \"'n'                  Sort by disk free space, press again to reverse\",\n    \"'t'                  Sort by total disk space, press again to reverse\",\n    \"'p'                  Sort by disk usage percentage, press again to reverse\",\n    \"'r'                  Sort by disk read activity, press again to reverse\",\n    \"'w'                  Sort by disk write activity, press again to reverse\",\n];\n\nconst BATTERY_HELP_TEXT: [&str; 3] = [\n    \"8 - Battery widget\",\n    \"Left                 Go to previous battery\",\n    \"Right                Go to next battery\",\n];\n\nconst BASIC_MEM_HELP_TEXT: [&str; 2] = [\n    \"9 - Basic memory widget\",\n    \"%                    Toggle between values and percentages for memory usage\",\n];\n\npub(crate) const HELP_TEXT: [&[&str]; HELP_CONTENTS_TEXT.len()] = [\n    &HELP_CONTENTS_TEXT,\n    &GENERAL_HELP_TEXT,\n    &CPU_HELP_TEXT,\n    &PROCESS_HELP_TEXT,\n    &SEARCH_HELP_TEXT,\n    &SORT_HELP_TEXT,\n    &TEMP_HELP_WIDGET,\n    &DISK_HELP_WIDGET,\n    &BATTERY_HELP_TEXT,\n    &BASIC_MEM_HELP_TEXT,\n];\n\npub(crate) const DEFAULT_LAYOUT: &str = r#\"\n[[row]]\n  ratio=30\n  [[row.child]]\n  type=\"cpu\"\n[[row]]\n    ratio=40\n    [[row.child]]\n      ratio=4\n      type=\"mem\"\n    [[row.child]]\n      ratio=3\n      [[row.child.child]]\n        type=\"temp\"\n      [[row.child.child]]\n        type=\"disk\"\n[[row]]\n  ratio=30\n  [[row.child]]\n    type=\"net\"\n  [[row.child]]\n    type=\"proc\"\n    default=true\n\"#;\n\npub(crate) const DEFAULT_BATTERY_LAYOUT: &str = r#\"\n[[row]]\n  ratio=30\n  [[row.child]]\n    ratio=2\n    type=\"cpu\"\n  [[row.child]]\n    ratio=1\n    type=\"battery\"\n[[row]]\n    ratio=40\n    [[row.child]]\n      ratio=4\n      type=\"mem\"\n    [[row.child]]\n      ratio=3\n      [[row.child.child]]\n        type=\"temp\"\n      [[row.child.child]]\n        type=\"disk\"\n[[row]]\n  ratio=30\n  [[row.child]]\n    type=\"net\"\n  [[row.child]]\n    type=\"proc\"\n    default=true\n\"#;\n\n// TODO: Eventually deprecate this, or grab from a file.\npub(crate) const CONFIG_TEXT: &str = r#\"# This is a default config file for bottom. All of the settings are commented\n# out by default; if you wish to change them uncomment and modify as you see\n# fit.\n\n# This group of options represents a command-line option. Flags explicitly\n# added when running (ie: btm -a) will override this config file if an option\n# is also set here.\n[flags]\n# Whether to hide the average cpu entry.\n#hide_avg_cpu = false\n\n# Whether to use a dedicated row for the average cpu entry\n#average_cpu_row = false\n\n# Whether to use dot markers rather than braille.\n#dot_marker = false\n\n# The update rate of the application.\n#rate = \"1s\"\n\n# Whether to put the CPU legend to the left.\n#cpu_left_legend = false\n\n# Whether to set CPU% on a process to be based on the total CPU or just current usage.\n#current_usage = false\n\n# Whether to set CPU% on a process to be based on the total CPU or per-core CPU% (not divided by the number of cpus).\n#unnormalized_cpu = false\n\n# Whether to group processes with the same name together by default. Doesn't do anything\n# if tree is set to true or --tree is set.\n#group_processes = false\n\n# Whether to make process searching case sensitive by default.\n#case_sensitive = false\n\n# Whether to make process searching look for matching the entire word by default.\n#whole_word = false\n\n# Whether to make process searching use regex by default.\n#regex = false\n\n# The temperature unit. One of the following, defaults to \"c\" for Celsius:\n#temperature_type = \"c\"\n##temperature_type = \"k\"\n##temperature_type = \"f\"\n##temperature_type = \"kelvin\"\n##temperature_type = \"fahrenheit\"\n##temperature_type = \"celsius\"\n\n# The default time interval (in milliseconds).\n#default_time_value = \"60s\"\n\n# The time delta on each zoom in/out action (in milliseconds).\n#time_delta = 15000\n\n# Hides the time scale.\n#hide_time = false\n\n# Override layout default widget\n#default_widget_type = \"proc\"\n#default_widget_count = 1\n\n# Expand selected widget upon starting the app\n#expanded = true\n\n# Use basic mode\n#basic = false\n\n# Use the old network legend style\n#use_old_network_legend = false\n\n# Remove space in tables\n#hide_table_gap = false\n\n# Show the battery widgets\n#battery = false\n\n# Disable mouse clicks\n#disable_click = false\n\n# Disable keyboard shortcuts\n#disable_keys = false\n\n# Show memory values in the processes widget as values by default\n#process_memory_as_value = false\n\n# Show tree mode by default in the processes widget.\n#tree = false\n\n# Shows an indicator in table widgets tracking where in the list you are.\n#show_table_scroll_position = false\n\n# Show processes as their commands by default in the process widget.\n#process_command = false\n\n# Displays the network widget with binary prefixes.\n#network_use_binary_prefix = false\n\n# Displays the network widget using bytes.\n#network_use_bytes = false\n\n# Displays the network widget with a log scale.\n#network_use_log = false\n\n# Hides advanced options to stop a process on Unix-like systems.\n#disable_advanced_kill = false\n\n# Prevents performing any actions that affect the system (e.g. stopping processes).\n#read_only = false\n\n# Hides the kernel threads\n#hide_k_threads = false\n\n# Hide GPU(s) information\n#disable_gpu = false\n\n# Shows cache and buffer memory\n#enable_cache_memory = false\n\n# Subtract freeable ARC from memory usage\n#free_arc = false\n\n# How much data is stored at once in terms of time.\n#retention = \"10m\"\n\n# Where to place the legend for the memory widget. One of \"none\", \"top-left\", \"top\", \"top-right\", \"left\", \"right\", \"bottom-left\", \"bottom\", \"bottom-right\".\n#memory_legend = \"top-right\"\n\n# Where to place the legend for the network widget. One of \"none\", \"top-left\", \"top\", \"top-right\", \"left\", \"right\", \"bottom-left\", \"bottom\", \"bottom-right\".\n#network_legend = \"top-right\"\n\n\n# Processes widget configuration\n#[processes]\n# The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built):\n# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%, Nice, Priority\n#columns = [\"PID\", \"Name\", \"CPU%\", \"Mem%\", \"Virt\", \"R/s\", \"W/s\", \"T.Read\", \"T.Write\", \"User\", \"State\", \"GMem%\", \"GPU%\", \"Priority\"]\n\n# Gather process child thread information\n#get_threads = false\n\n\n# CPU widget configuration\n#[cpu]\n# One of \"all\" (default), \"average\"/\"avg\"\n#default = \"average\"\n\n\n# Disk widget configuration\n#[disk]\n# The columns shown by the process widget. The following columns are supported:\n# Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s\n#columns = [\"Disk\", \"Mount\", \"Used\", \"Free\", \"Total\", \"Used%\", \"R/s\", \"W/s\"]\n\n# By default, there are no disk name filters enabled. These can be turned on to filter out specific data entries if you\n# don't want to see them. An example use case is provided below.\n#[disk.name_filter]\n# Whether to ignore any matches. Defaults to true.\n#is_list_ignored = true\n\n# A list of filters to try and match.\n#list = [\"/dev/sda\\\\d+\", \"/dev/nvme0n1p2\"]\n\n# Whether to use regex. Defaults to false.\n#regex = true\n\n# Whether to be case-sensitive. Defaults to false.\n#case_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\n#whole_word = false\n\n# By default, there are no mount name filters enabled. An example use case is provided below.\n#[disk.mount_filter]\n# Whether to ignore any matches. Defaults to true.\n#is_list_ignored = true\n\n# A list of filters to try and match.\n#list = [\"/mnt/.*\", \"/boot\"]\n\n# Whether to use regex. Defaults to false.\n#regex = true\n\n# Whether to be case-sensitive. Defaults to false.\n#case_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\n#whole_word = false\n\n\n# Temperature widget configuration\n#[temperature]\n# By default, there are no temperature sensor filters enabled. An example use case is provided below.\n#[temperature.sensor_filter]\n# Whether to ignore any matches. Defaults to true.\n#is_list_ignored = true\n\n# A list of filters to try and match.\n#list = [\"cpu\", \"wifi\"]\n\n# Whether to use regex. Defaults to false.\n#regex = false\n\n# Whether to be case-sensitive. Defaults to false.\n#case_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\n#whole_word = false\n\n\n# Network widget configuration\n#[network]\n# By default, there are no network interface filters enabled. An example use case is provided below.\n#[network.interface_filter]\n# Whether to ignore any matches. Defaults to true.\n#is_list_ignored = true\n\n# A list of filters to try and match.\n#list = [\"virbr0.*\"]\n\n# Whether to use regex. Defaults to false.\n#regex = true\n\n# Whether to be case-sensitive. Defaults to false.\n#case_sensitive = false\n\n# Whether to be require matching the whole word. Defaults to false.\n#whole_word = false\n\n\n# These are all the components that support custom theming.  Note that colour support\n# will depend on terminal support.\n#[styles] # Uncomment if you want to use custom styling\n# Built-in themes. Valid values are:\n# - \"default\"\n# - \"default-light\"\n# - \"gruvbox\"\n# - \"gruvbox-light\"\n# - \"nord\"\n# - \"nord-light\".\n#\n# This will have the lowest precedence if a custom colour palette is set,\n# or overridden if the command-line flag for a built-in theme is set.\n#theme = \"default\"\n\n#[styles.cpu]\n#all_entry_color = \"green\"\n#avg_entry_color = \"red\"\n#cpu_core_colors = [\"light magenta\", \"light yellow\", \"light cyan\", \"light green\", \"light blue\", \"cyan\", \"green\", \"blue\"]\n\n#[styles.memory]\n#ram_color = \"light magenta\"\n#cache_color = \"light red\"\n#swap_color = \"light yellow\"\n#arc_color = \"light cyan\"\n#gpu_colors = [\"light blue\", \"light red\", \"cyan\", \"green\", \"blue\", \"red\"]\n\n#[styles.network]\n#rx_color = \"light magenta\"\n#tx_color = \"light yellow\"\n#rx_total_color = \"light cyan\"\n#tx_total_color = \"light green\"\n\n#[styles.battery]\n#high_battery_color = \"green\"\n#medium_battery_color = \"yellow\"\n#low_battery_color = \"red\"\n\n#[styles.tables]\n#headers = {color = \"light blue\", bold = true}\n\n#[styles.graphs]\n#graph_color = \"gray\"\n#legend_text = {color = \"gray\"}\n\n#[styles.widgets]\n#border_color = \"gray\"\n#selected_border_color = \"light blue\"\n#widget_title = {color = \"gray\"}\n#text = {color = \"gray\"}\n#selected_text = {color = \"black\", bg_color = \"light blue\"}\n#disabled_text = {color = \"dark gray\"}\n\n# Only on Linux\n#thread_text = {color = \"green\"}\n\n# Layout - layouts follow a pattern like this:\n# [[row]] represents a row in the application.\n# [[row.child]] represents either a widget or a column.\n# [[row.child.child]] represents a widget.\n#\n# All widgets must have the type value set to one of [\"cpu\", \"mem\", \"proc\", \"net\", \"temp\", \"disk\", \"empty\"].\n# All layout components have a ratio value - if this is not set, then it defaults to 1.\n# The default widget layout:\n#[[row]]\n#  ratio=30\n#  [[row.child]]\n#  type=\"cpu\"\n#[[row]]\n#    ratio=40\n#    [[row.child]]\n#      ratio=4\n#      type=\"mem\"\n#    [[row.child]]\n#      ratio=3\n#      [[row.child.child]]\n#        type=\"temp\"\n#      [[row.child.child]]\n#        type=\"disk\"\n#[[row]]\n#  ratio=30\n#  [[row.child]]\n#    type=\"net\"\n#  [[row.child]]\n#    type=\"proc\"\n#    default=true\n\"#;\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn help_menu_matches_entry_len() {\n        // The two match since HELP_TEXT contains HELP_CONTENTS_TEXT as an entry\n        assert_eq!(\n            HELP_CONTENTS_TEXT.len(),\n            HELP_TEXT.len(),\n            \"the two should be equal, or this test should be updated\"\n        )\n    }\n\n    #[test]\n    fn help_menu_text_has_sections() {\n        for (itx, line) in HELP_TEXT.iter().enumerate() {\n            if itx > 0 {\n                assert!(line.len() >= 2, \"each section should be at least 2 lines\");\n                assert!(line[0].contains(\" - \"), \"each section should have a header\");\n            }\n        }\n    }\n\n    /// Checks that the default config is valid.\n    #[test]\n    #[cfg(feature = \"default\")]\n    fn check_default_config() {\n        use regex::Regex;\n\n        use crate::options::Config;\n\n        let default_config = Regex::new(r\"(?m)^#([a-zA-Z\\[])\")\n            .unwrap()\n            .replace_all(CONFIG_TEXT, \"$1\");\n\n        let default_config = Regex::new(r\"(?m)^#(\\s\\s+)([a-zA-Z\\[])\")\n            .unwrap()\n            .replace_all(&default_config, \"$2\");\n\n        let _config: Config =\n            toml_edit::de::from_str(&default_config).expect(\"can parse default config\");\n\n        // TODO: Check this.\n        // assert_eq!(config, Config::default());\n    }\n}\n"
  },
  {
    "path": "src/event.rs",
    "content": "//! Some code around handling events.\n\nuse std::sync::mpsc::Sender;\n\nuse crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};\n\nuse crate::{\n    app::{App, layout_manager::WidgetDirection},\n    collection::Data,\n};\n\n/// Events sent to the main thread.\n#[derive(Debug)]\npub enum BottomEvent {\n    Resize,\n    KeyInput(KeyEvent),\n    MouseInput(MouseEvent),\n    PasteEvent(String),\n    Update(Box<Data>),\n    Clean,\n    Terminate,\n}\n\n/// Events sent to the collection thread.\n#[derive(Debug)]\npub enum CollectionThreadEvent {\n    Reset,\n}\n\n/// Handle a [`MouseEvent`].\npub fn handle_mouse_event(event: MouseEvent, app: &mut App) {\n    match event.kind {\n        MouseEventKind::ScrollUp => app.handle_scroll_up(),\n        MouseEventKind::ScrollDown => app.handle_scroll_down(),\n        MouseEventKind::Down(button) => {\n            let (x, y) = (event.column, event.row);\n            if !app.app_config_fields.disable_click {\n                match button {\n                    crossterm::event::MouseButton::Left => {\n                        // Trigger left click widget activity\n                        app.on_left_mouse_up(x, y);\n                    }\n                    crossterm::event::MouseButton::Right => {}\n                    _ => {}\n                }\n            }\n        }\n        _ => {}\n    };\n}\n\n/// Handle a [`KeyEvent`].\npub fn handle_key_event_or_break(\n    event: KeyEvent, app: &mut App, reset_sender: &Sender<CollectionThreadEvent>,\n) -> bool {\n    // c_debug!(\"KeyEvent: {event:?}\");\n\n    if event.modifiers.is_empty() {\n        match event.code {\n            KeyCode::Char('q') if !app.is_in_search_widget() => return true,\n            KeyCode::End => app.skip_to_last(),\n            KeyCode::Home => app.skip_to_first(),\n            KeyCode::Up => app.on_up_key(),\n            KeyCode::Down => app.on_down_key(),\n            KeyCode::Left => app.on_left_key(),\n            KeyCode::Right => app.on_right_key(),\n            KeyCode::Char(' ') if !app.is_in_search_widget() => app.on_space_key(),\n            KeyCode::Char(caught_char) => app.on_char_key(caught_char),\n            KeyCode::Esc => app.on_esc(),\n            KeyCode::Enter => app.on_enter(),\n            KeyCode::Tab => app.on_tab(),\n            KeyCode::Backspace => app.on_backspace(),\n            KeyCode::Delete => app.on_delete(),\n            KeyCode::F(1) => app.toggle_ignore_case(),\n            KeyCode::F(2) => app.toggle_search_whole_word(),\n            KeyCode::F(3) => app.toggle_search_regex(),\n            KeyCode::F(5) => app.toggle_tree_mode(),\n            KeyCode::F(6) => app.toggle_sort_menu(),\n            KeyCode::F(9) => app.kill_current_process(),\n            KeyCode::PageDown => app.on_page_down(),\n            KeyCode::PageUp => app.on_page_up(),\n            _ => {}\n        }\n    } else {\n        // Otherwise, track the modifier as well...\n        if let KeyModifiers::ALT = event.modifiers {\n            match event.code {\n                KeyCode::Char('c') | KeyCode::Char('C') => app.toggle_ignore_case(),\n                KeyCode::Char('w') | KeyCode::Char('W') => app.toggle_search_whole_word(),\n                KeyCode::Char('r') | KeyCode::Char('R') => app.toggle_search_regex(),\n                KeyCode::Char('h') => app.on_left_key(),\n                KeyCode::Char('l') => app.on_right_key(),\n                _ => {}\n            }\n        } else if let KeyModifiers::CONTROL = event.modifiers {\n            if event.code == KeyCode::Char('c') {\n                return true;\n            }\n\n            match event.code {\n                KeyCode::Char('f') => app.on_slash(),\n                KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left),\n                KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right),\n                KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up),\n                KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down),\n                KeyCode::Char('r') if reset_sender.send(CollectionThreadEvent::Reset).is_ok() => {\n                    app.reset();\n                }\n                KeyCode::Char('a') => app.skip_cursor_beginning(),\n                KeyCode::Char('e') => app.skip_cursor_end(),\n                KeyCode::Char('u') if app.is_in_search_widget() => app.clear_search(),\n                KeyCode::Char('w') => app.clear_previous_word(),\n                KeyCode::Char('h') => app.on_backspace(),\n                KeyCode::Char('d') => app.scroll_half_page_down(),\n                KeyCode::Char('u') => app.scroll_half_page_up(),\n                // KeyCode::Char('j') => {}, // Move down\n                // KeyCode::Char('k') => {}, // Move up\n                // KeyCode::Char('h') => {}, // Move right\n                // KeyCode::Char('l') => {}, // Move left\n                // Can't do now, CTRL+BACKSPACE doesn't work and graphemes\n                // are hard to iter while truncating last (eloquently).\n                // KeyCode::Backspace => app.skip_word_backspace(),\n                _ => {}\n            }\n        } else if let KeyModifiers::SHIFT = event.modifiers {\n            match event.code {\n                KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left),\n                KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right),\n                KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up),\n                KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down),\n                KeyCode::Char(caught_char) => app.on_char_key(caught_char),\n                _ => {}\n            }\n        }\n    }\n\n    false\n}\n"
  },
  {
    "path": "src/lib.rs",
    "content": "//! A customizable cross-platform graphical process/system monitor for the\n//! terminal. Supports Linux, macOS, and Windows. Inspired by gtop, gotop, and\n//! htop.\n//!\n//! **Note:** The following documentation is primarily intended for people to\n//! refer to for development purposes rather than the actual usage of the\n//! application. If you are instead looking for documentation regarding the\n//! *usage* of bottom, refer to [here](https://bottom.pages.dev/stable/).\n\npub(crate) mod app;\nmod utils {\n    pub(crate) mod cancellation_token;\n    pub(crate) mod conversion;\n    pub(crate) mod data_units;\n    pub(crate) mod general;\n    pub(crate) mod logging;\n    pub(crate) mod process_killer;\n    pub(crate) mod strings;\n}\npub(crate) mod canvas;\npub(crate) mod collection;\npub(crate) mod constants;\npub(crate) mod event;\npub mod options;\npub mod widgets;\n\nuse std::{\n    boxed::Box,\n    io::{Write, stderr, stdout},\n    panic::{self, PanicHookInfo},\n    sync::{\n        Arc,\n        mpsc::{self, Receiver, Sender},\n    },\n    thread::{self, JoinHandle},\n    time::{Duration, Instant},\n};\n\nuse app::{App, AppConfigFields, DataFilters, layout_manager::UsedWidgets};\nuse crossterm::{\n    cursor::{Hide, Show},\n    event::{\n        DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,\n        Event, KeyEventKind, MouseEventKind, poll, read,\n    },\n    execute,\n    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},\n};\nuse event::{BottomEvent, CollectionThreadEvent, handle_key_event_or_break, handle_mouse_event};\nuse options::{args, get_or_create_config, init_app};\nuse tui::{Terminal, backend::CrosstermBackend};\n#[allow(unused_imports, reason = \"this is needed if logging is enabled\")]\nuse utils::logging::*;\nuse utils::{cancellation_token::CancellationToken, conversion::*};\n\nuse crate::collection::Data;\n\n// Used for heap allocation debugging purposes.\n// #[global_allocator]\n// static ALLOC: dhat::Alloc = dhat::Alloc;\n\n/// Try drawing. If not, clean up the terminal and return an error.\nfn try_drawing(\n    terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>, app: &mut App,\n    painter: &mut canvas::Painter,\n) -> anyhow::Result<()> {\n    if let Err(err) = painter.draw_data(terminal, app) {\n        cleanup_terminal(terminal)?;\n        Err(err.into())\n    } else {\n        Ok(())\n    }\n}\n\n/// Clean up the terminal before returning it to the user.\nfn cleanup_terminal(\n    terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,\n) -> anyhow::Result<()> {\n    disable_raw_mode()?;\n\n    execute!(\n        terminal.backend_mut(),\n        DisableMouseCapture,\n        DisableBracketedPaste,\n        LeaveAlternateScreen,\n        Show,\n    )?;\n    terminal.show_cursor()?;\n\n    Ok(())\n}\n\n/// Check and report to the user if the current environment is not a terminal.\nfn check_if_terminal() {\n    use crossterm::tty::IsTty;\n\n    if !stdout().is_tty() {\n        eprintln!(\n            \"Warning: bottom is not being output to a terminal. Things might not work properly.\"\n        );\n        eprintln!(\"If you're stuck, press 'q' or 'Ctrl-c' to quit the program.\");\n        stderr().flush().expect(\"should succeed in flushing stderr\");\n        thread::sleep(Duration::from_secs(1));\n    }\n}\n\n/// This manually resets stdout back to normal state.\npub fn reset_stdout() {\n    let mut stdout = stdout();\n    let _ = disable_raw_mode();\n    let _ = execute!(\n        stdout,\n        DisableMouseCapture,\n        DisableBracketedPaste,\n        LeaveAlternateScreen,\n        Show,\n    );\n}\n\n/// A panic hook to properly restore the terminal in the case of a panic.\n/// Originally based on [spotify-tui's implementation](https://github.com/Rigellute/spotify-tui/blob/master/src/main.rs).\nfn panic_hook(panic_info: &PanicHookInfo<'_>) {\n    let msg = match panic_info.payload().downcast_ref::<&'static str>() {\n        Some(s) => *s,\n        None => match panic_info.payload().downcast_ref::<String>() {\n            Some(s) => &s[..],\n            None => \"Box<Any>\",\n        },\n    };\n\n    let backtrace = format!(\"{:?}\", std::backtrace::Backtrace::capture());\n\n    reset_stdout();\n\n    // Print stack trace. Must be done after!\n    if let Some(panic_info) = panic_info.location() {\n        println!(\"thread '<unnamed>' panicked at '{msg}', {panic_info}\\n\\r{backtrace}\")\n    }\n\n    // TODO: Might be cleaner in the future to use a cancellation token, but that causes some fun issues with\n    // lifetimes; for now if it panics then shut down the main program entirely ASAP.\n    std::process::exit(1);\n}\n\n/// Create a thread to poll for user inputs and forward them to the main thread.\nfn create_input_thread(\n    sender: Sender<BottomEvent>, cancellation_token: Arc<CancellationToken>,\n    app_config_fields: &AppConfigFields,\n) -> JoinHandle<()> {\n    let keys_disabled = app_config_fields.disable_keys;\n\n    thread::spawn(move || {\n        let mut mouse_timer = Instant::now();\n\n        loop {\n            // We don't block.\n            if let Some(is_terminated) = cancellation_token.try_check() {\n                if is_terminated {\n                    break;\n                }\n            }\n\n            if let Ok(poll) = poll(Duration::from_millis(20)) {\n                if poll {\n                    if let Ok(event) = read() {\n                        match event {\n                            Event::Resize(_, _) => {\n                                // TODO: Might want to debounce this in the future, or take into\n                                // account the actual resize values.\n                                // Maybe we want to keep the current implementation in case the\n                                // resize event might not fire...\n                                // not sure.\n\n                                if sender.send(BottomEvent::Resize).is_err() {\n                                    break;\n                                }\n                            }\n                            Event::Paste(paste) => {\n                                if sender.send(BottomEvent::PasteEvent(paste)).is_err() {\n                                    break;\n                                }\n                            }\n                            Event::Key(key)\n                                if !keys_disabled && key.kind == KeyEventKind::Press =>\n                            {\n                                // For now, we only care about key down events. This may change in\n                                // the future.\n                                if sender.send(BottomEvent::KeyInput(key)).is_err() {\n                                    break;\n                                }\n                            }\n                            Event::Mouse(mouse) => match mouse.kind {\n                                MouseEventKind::Moved | MouseEventKind::Drag(..) => {}\n                                MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => {\n                                    if Instant::now().duration_since(mouse_timer).as_millis() >= 20\n                                    {\n                                        if sender.send(BottomEvent::MouseInput(mouse)).is_err() {\n                                            break;\n                                        }\n                                        mouse_timer = Instant::now();\n                                    }\n                                }\n                                _ => {\n                                    if sender.send(BottomEvent::MouseInput(mouse)).is_err() {\n                                        break;\n                                    }\n                                }\n                            },\n                            Event::Key(_) => {}\n                            Event::FocusGained => {}\n                            Event::FocusLost => {}\n                        }\n                    }\n                }\n            }\n        }\n    })\n}\n\n/// Create a thread to handle data collection.\nfn create_collection_thread(\n    sender: Sender<BottomEvent>, control_receiver: Receiver<CollectionThreadEvent>,\n    cancellation_token: Arc<CancellationToken>, app_config_fields: &AppConfigFields,\n    filters: DataFilters, used_widget_set: UsedWidgets,\n) -> JoinHandle<()> {\n    let use_current_cpu_total = app_config_fields.use_current_cpu_total;\n    let unnormalized_cpu = app_config_fields.unnormalized_cpu;\n    let show_average_cpu = app_config_fields.show_average_cpu;\n    let update_sleep = app_config_fields.update_rate;\n    let get_process_threads = app_config_fields.get_process_threads;\n    #[cfg(feature = \"zfs\")]\n    let get_arc_free = app_config_fields.free_arc;\n\n    thread::spawn(move || {\n        let mut data_collector = collection::DataCollector::new(filters);\n\n        data_collector.set_collection(used_widget_set);\n        data_collector.set_use_current_cpu_total(use_current_cpu_total);\n        data_collector.set_unnormalized_cpu(unnormalized_cpu);\n        data_collector.set_show_average_cpu(show_average_cpu);\n        data_collector.set_get_process_threads(get_process_threads);\n        #[cfg(feature = \"zfs\")]\n        data_collector.set_free_arc_mem(get_arc_free);\n\n        data_collector.update_data();\n        data_collector.data = Data::default();\n\n        // Tiny sleep I guess? To go between the first update above and the first update in the loop.\n        std::thread::sleep(Duration::from_millis(5));\n\n        loop {\n            // Check once at the very top... don't block though.\n            if let Some(is_terminated) = cancellation_token.try_check() {\n                if is_terminated {\n                    break;\n                }\n            }\n\n            if let Ok(message) = control_receiver.try_recv() {\n                // trace!(\"Received message in collection thread: {message:?}\");\n                match message {\n                    CollectionThreadEvent::Reset => {\n                        data_collector.data.cleanup();\n                    }\n                }\n            }\n\n            data_collector.update_data();\n\n            // Yet another check to bail if needed... do not block!\n            if let Some(is_terminated) = cancellation_token.try_check() {\n                if is_terminated {\n                    break;\n                }\n            }\n\n            let event = BottomEvent::Update(Box::from(data_collector.data));\n            data_collector.data = Data::default();\n\n            if sender.send(event).is_err() {\n                break;\n            }\n\n            // Sleep while allowing for interruptions...\n            if cancellation_token.sleep_with_cancellation(Duration::from_millis(update_sleep)) {\n                break;\n            }\n        }\n    })\n}\n\n/// Main code to call to start bottom.\n#[inline]\npub fn start_bottom(enable_error_hook: &mut bool) -> anyhow::Result<()> {\n    // let _profiler = dhat::Profiler::new_heap();\n\n    let args = args::get_args();\n\n    #[cfg(feature = \"logging\")]\n    {\n        if let Err(err) = init_logger(\n            log::LevelFilter::Debug,\n            Some(std::ffi::OsStr::new(\"debug.log\")),\n        ) {\n            println!(\"Issue initializing logger: {err}\");\n        }\n    }\n\n    // Read from config file.\n    let config = get_or_create_config(args.general.config_location.as_deref())?;\n\n    // Create the \"app\" and initialize a bunch of stuff.\n    let (mut app, widget_layout, styling) = init_app(args, config)?;\n\n    // Create painter and set colours.\n    let mut painter = canvas::Painter::init(widget_layout, styling)?;\n\n    // Check if the current environment is in a terminal.\n    check_if_terminal();\n\n    let cancellation_token = Arc::new(CancellationToken::default());\n    let (sender, receiver) = mpsc::channel();\n\n    // Set up the event loop thread; we set this up early to speed up\n    // first-time-to-data.\n    let (collection_thread_ctrl_sender, collection_thread_ctrl_receiver) = mpsc::channel();\n    let _collection_thread = create_collection_thread(\n        sender.clone(),\n        collection_thread_ctrl_receiver,\n        cancellation_token.clone(),\n        &app.app_config_fields,\n        app.filters.clone(),\n        app.used_widgets,\n    );\n\n    // Set up the input handling loop thread.\n    let _input_thread = create_input_thread(\n        sender.clone(),\n        cancellation_token.clone(),\n        &app.app_config_fields,\n    );\n\n    // Set up the cleaning loop thread.\n    let _cleaning_thread = {\n        let cancellation_token = cancellation_token.clone();\n        let cleaning_sender = sender.clone();\n        let offset_wait = Duration::from_millis(app.app_config_fields.retention_ms + 60000);\n        thread::spawn(move || {\n            loop {\n                if cancellation_token.sleep_with_cancellation(offset_wait) {\n                    break;\n                }\n\n                if cleaning_sender.send(BottomEvent::Clean).is_err() {\n                    break;\n                }\n            }\n        })\n    };\n\n    // Set up tui and crossterm\n    *enable_error_hook = true;\n\n    let mut stdout_val = stdout();\n    execute!(stdout_val, Hide, EnterAlternateScreen, EnableBracketedPaste)?;\n    if app.app_config_fields.disable_click {\n        execute!(stdout_val, DisableMouseCapture)?;\n    } else {\n        execute!(stdout_val, EnableMouseCapture)?;\n    }\n    enable_raw_mode()?;\n\n    let mut terminal = Terminal::new(CrosstermBackend::new(stdout_val))?;\n    terminal.clear()?;\n    terminal.hide_cursor()?;\n\n    #[cfg(target_os = \"freebsd\")]\n    let _stderr_fd = {\n        // A really ugly band-aid to suppress stderr warnings on FreeBSD due to sysinfo.\n        // For more information, see https://github.com/ClementTsang/bottom/issues/798.\n        use std::fs::OpenOptions;\n\n        use filedescriptor::{FileDescriptor, StdioDescriptor};\n\n        let path = OpenOptions::new().write(true).open(\"/dev/null\")?;\n        FileDescriptor::redirect_stdio(&path, StdioDescriptor::Stderr)?\n    };\n\n    // Set panic hook\n    panic::set_hook(Box::new(panic_hook));\n\n    // Set termination hook\n    ctrlc::set_handler(move || {\n        // TODO: Consider using signal-hook (https://github.com/vorner/signal-hook) to handle\n        // more types of signals?\n        let _ = sender.send(BottomEvent::Terminate);\n    })?;\n\n    let mut first_run = true;\n\n    // Draw once first to initialize the canvas, so it doesn't feel like it's\n    // frozen.\n    try_drawing(&mut terminal, &mut app, &mut painter)?;\n\n    loop {\n        if let Ok(recv) = receiver.recv() {\n            match recv {\n                BottomEvent::Terminate => break,\n                BottomEvent::Resize => {\n                    try_drawing(&mut terminal, &mut app, &mut painter)?;\n                }\n                BottomEvent::KeyInput(event) => {\n                    if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) {\n                        break;\n                    }\n                    app.update_data();\n                    try_drawing(&mut terminal, &mut app, &mut painter)?;\n                }\n                BottomEvent::MouseInput(event) => {\n                    handle_mouse_event(event, &mut app);\n                    app.update_data();\n                    try_drawing(&mut terminal, &mut app, &mut painter)?;\n                }\n                BottomEvent::PasteEvent(paste) => {\n                    app.handle_paste(paste);\n                    app.update_data();\n                    try_drawing(&mut terminal, &mut app, &mut painter)?;\n                }\n                BottomEvent::Update(data) => {\n                    app.data_store.eat_data(data, &app.app_config_fields);\n\n                    // This thing is required as otherwise, some widgets can't draw correctly w/o\n                    // some data (or they need to be re-drawn).\n                    if first_run {\n                        first_run = false;\n                        app.is_force_redraw = true;\n                    }\n\n                    if !app.data_store.is_frozen() {\n                        // Convert all data into data for the displayed widgets.\n\n                        if app.used_widgets.use_disk {\n                            for disk in app.states.disk_state.widget_states.values_mut() {\n                                disk.force_data_update();\n                            }\n                        }\n\n                        if app.used_widgets.use_temp {\n                            for temp in app.states.temp_state.widget_states.values_mut() {\n                                temp.force_data_update();\n                            }\n                        }\n\n                        if app.used_widgets.use_proc {\n                            for proc in app.states.proc_state.widget_states.values_mut() {\n                                proc.force_data_update();\n                            }\n                        }\n\n                        if app.used_widgets.use_cpu {\n                            for cpu in app.states.cpu_state.widget_states.values_mut() {\n                                cpu.force_data_update();\n                            }\n                        }\n\n                        app.update_data();\n                        try_drawing(&mut terminal, &mut app, &mut painter)?;\n                    }\n                }\n                BottomEvent::Clean => {\n                    app.data_store\n                        .clean_data(Duration::from_millis(app.app_config_fields.retention_ms));\n                }\n            }\n        }\n    }\n\n    // I think doing it in this order is safe...\n    // TODO: maybe move the cancellation token to the ctrl-c handler?\n    cancellation_token.cancel();\n    cleanup_terminal(&mut terminal)?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/options/args.rs",
    "content": "//! Argument parsing via clap.\n//!\n//! Note that you probably want to keep this as a single file so the build\n//! script doesn't trip all over itself.\n\n// TODO: New sections are misaligned! See if we can get that fixed.\n\nuse std::path::PathBuf;\n\nuse clap::{builder::PossibleValue, *};\nuse indoc::indoc;\n\nconst TEMPLATE: &str = indoc! {\n    \"{name} {version}\n    {author}\n\n    {about}\n\n    {usage-heading} {usage}\n\n    {all-args}\"\n};\n\nconst USAGE: &str = \"btm [OPTIONS]\";\n\nconst VERSION: &str = match option_env!(\"NIGHTLY_VERSION\") {\n    Some(nightly_version) => nightly_version,\n    None => crate_version!(),\n};\n\nconst CHART_WIDGET_POSITIONS: [&str; 9] = [\n    \"none\",\n    \"top-left\",\n    \"top\",\n    \"top-right\",\n    \"left\",\n    \"right\",\n    \"bottom-left\",\n    \"bottom\",\n    \"bottom-right\",\n];\n\n/// Represents the arguments that can be passed in to bottom.\n#[derive(Parser, Debug)]\n#[command(\n    name = crate_name!(),\n    version = VERSION,\n    author = crate_authors!(),\n    about = crate_description!(),\n    disable_help_flag = true,\n    disable_version_flag = true,\n    color = ColorChoice::Auto,\n    help_template = TEMPLATE,\n    override_usage = USAGE,\n)]\npub struct BottomArgs {\n    #[command(flatten)]\n    pub general: GeneralArgs,\n\n    #[command(flatten)]\n    pub process: ProcessArgs,\n\n    #[command(flatten)]\n    pub temperature: TemperatureArgs,\n\n    #[command(flatten)]\n    pub cpu: CpuArgs,\n\n    #[command(flatten)]\n    pub memory: MemoryArgs,\n\n    #[command(flatten)]\n    pub network: NetworkArgs,\n\n    #[cfg(feature = \"battery\")]\n    #[command(flatten)]\n    pub battery: BatteryArgs,\n\n    #[cfg(feature = \"gpu\")]\n    #[command(flatten)]\n    pub gpu: GpuArgs,\n\n    #[command(flatten)]\n    pub style: StyleArgs,\n\n    #[command(flatten)]\n    pub other: OtherArgs,\n}\n\n/// General arguments/config options.\n#[derive(Args, Clone, Debug)]\n#[command(next_help_heading = \"General Options\", rename_all = \"snake_case\")]\npub struct GeneralArgs {\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Temporarily shows the time scale in graphs.\",\n        long_help = \"Automatically hides the time scale in graphs after being shown for a brief moment when zoomed \\\n                in/out. If time is disabled using --hide_time then this will have no effect.\",\n        alias = \"autohide-time\"\n    )]\n    pub autohide_time: bool,\n\n    #[arg(\n        short = 'b',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Hides graphs and uses a more basic look.\",\n        long_help = \"Hides graphs and uses a more basic look, largely inspired by htop's design.\"\n    )]\n    pub basic: bool,\n\n    #[arg(\n        short = 'C',\n        long,\n        value_name = \"PATH\",\n        value_hint = ValueHint::AnyPath,\n        help = \"Sets the location of the config file.\",\n        long_help = \"Sets the location of the config file. Expects a config file in the TOML format. \\\n                    If it doesn't exist, a default config file is created at the path. If no path is provided, \\\n                    the default config location will be used.\",\n        alias = \"config-location\",\n        alias = \"config\",\n    )]\n    pub config_location: Option<PathBuf>,\n\n    #[arg(\n        short = 't',\n        long,\n        value_name = \"TIME\",\n        help = \"Default time value for graphs.\",\n        long_help = \"Default time value for graphs. Either a number in milliseconds or a 'human duration' \\\n                (e.g. 60s, 10m). Defaults to 60s, must be at least 30s.\",\n        alias = \"default-time-value\"\n    )]\n    pub default_time_value: Option<String>,\n\n    // TODO: Charts are broken in the manpage\n    #[arg(\n        long,\n        requires_all = [\"default_widget_type\"],\n        value_name = \"N\",\n        help = \"Sets the N'th selected widget type as the default.\",\n        long_help = indoc! {\n            \"Sets the N'th selected widget type to use as the default widget. Requires 'default_widget_type' to also be \\\n            set, and defaults to 1.\n\n            This reads from left to right, top to bottom. For example, suppose we have a layout that looks like:\n            +-------------------+-----------------------+\n            |      CPU (1)      |        CPU (2)        |\n            +---------+---------+-------------+---------+\n            | Process | CPU (3) | Temperature | CPU (4) |\n            +---------+---------+-------------+---------+\n\n            And we set our default widget type to 'CPU'. If we set '--default_widget_count 1', then it would use the \\\n            CPU (1) as the default widget. If we set '--default_widget_count 3', it would use CPU (3) as the default \\\n            instead.\"\n        },\n        alias = \"default-widget-count\"\n    )]\n    pub default_widget_count: Option<u64>,\n\n    #[arg(\n        long,\n        value_name = \"WIDGET\",\n        help = \"Sets the default widget type. Use --help for more info.\",\n        long_help = indoc!{\n            \"Sets which widget type to use as the default widget. For the default \\\n            layout, this defaults to the 'process' widget. For a custom layout, it defaults \\\n            to the first widget it sees.\n\n            For example, suppose we have a layout that looks like:\n            +-------------------+-----------------------+\n            |      CPU (1)      |        CPU (2)        |\n            +---------+---------+-------------+---------+\n            | Process | CPU (3) | Temperature | CPU (4) |\n            +---------+---------+-------------+---------+\n\n            Then, setting '--default_widget_type temperature' will make the temperature widget selected by default.\"\n        },\n        value_parser = [\n            \"cpu\",\n            \"mem\",\n            \"net\",\n            \"network\",\n            \"proc\",\n            \"process\",\n            \"processes\",\n            \"temp\",\n            \"temperature\",\n            \"disk\",\n            #[cfg(feature = \"battery\")]\n            \"batt\",\n            #[cfg(feature = \"battery\")]\n            \"battery\",\n        ],\n        alias = \"default-widget-type\"\n    )]\n    pub default_widget_type: Option<String>,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Disables mouse clicks.\",\n        long_help = \"Disables mouse clicks from interacting with bottom.\",\n        alias = \"disable-click\"\n    )]\n    pub disable_click: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Disables keyboard shortcuts, INCLUDING the ones that stop bottom.\",\n        long_help = \"Disables keyboard shortcuts from interacting with bottom. Note this includes keyboard shortcuts to quit bottom.\",\n        alias = \"disable-keys\"\n    )]\n    pub disable_keys: bool,\n\n    // TODO: Change this to accept a string with the type of marker.\n    #[arg(\n        short = 'm',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Uses a dot marker for graphs.\",\n        long_help = \"Uses a dot marker for graphs as opposed to the default braille marker.\",\n        alias = \"dot-marker\"\n    )]\n    pub dot_marker: bool,\n\n    #[arg(\n        short = 'e',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Expand the default widget upon starting the app.\",\n        long_help = \"Expand the default widget upon starting the app. This flag has no effect in basic mode (--basic).\"\n    )]\n    pub expanded: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Hides spacing between table headers and entries.\",\n        alias = \"hide-table-gap\"\n    )]\n    pub hide_table_gap: bool,\n\n    #[arg(long, action = ArgAction::SetTrue, help = \"Hides the time scale from being shown.\", alias = \"hide-time\")]\n    pub hide_time: bool,\n\n    #[arg(\n        short = 'r',\n        long,\n        value_name = \"TIME\",\n        help = \"Sets how often data is refreshed.\",\n        long_help = \"Sets how often data is refreshed. Either a number in milliseconds or a 'human duration' \\\n                    (e.g. 1s, 1m). Defaults to 1s, must be at least 250ms. Smaller values may result in \\\n                    higher system resource usage.\"\n    )]\n    pub rate: Option<String>,\n\n    #[arg(\n        long,\n        value_name = \"TIME\",\n        help = \"How far back data will be stored up to.\",\n        long_help = \"How far back data will be stored up to. Either a number in milliseconds or a 'human duration' \\\n                    (e.g. 10m, 1h). Defaults to 10 minutes, and must be at least  1 minute. Larger values \\\n                    may result in higher memory usage.\"\n    )]\n    pub retention: Option<String>,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Shows the list scroll position tracker in the widget title for table widgets.\",\n        alias = \"show-table-scroll-position\"\n    )]\n    pub show_table_scroll_position: bool,\n\n    #[arg(\n        short = 'd',\n        long,\n        value_name = \"TIME\",\n        help = \"The amount of time changed upon zooming.\",\n        long_help = \"The amount of time changed when zooming in/out. Takes a number in \\\n                    milliseconds or a human duration (e.g. 30s). The minimum is 1s, and \\\n                    defaults to 15s.\",\n        alias = \"time-delta\"\n    )]\n    pub time_delta: Option<String>,\n}\n\n/// Process arguments/config options.\n#[derive(Args, Clone, Debug, Default)]\n#[command(next_help_heading = \"Process Options\", rename_all = \"snake_case\")]\npub struct ProcessArgs {\n    #[arg(\n        short = 'S',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Enables case sensitivity by default when searching.\",\n        long_help = \"Enables case sensitivity by default when searching for a process.\",\n        alias = \"case-sensitive\"\n    )]\n    pub case_sensitive: bool,\n\n    // TODO: Rename this.\n    #[arg(\n        short = 'u',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Calculates process CPU usage as a percentage of current usage rather than total usage.\",\n        alias = \"current-usage\"\n    )]\n    pub current_usage: bool,\n\n    #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Hides additional stopping options on Unix-like systems.\",\n        long_help = \"Hides additional stopping options on Unix-like systems. Signal 15 (TERM) will be sent when \\\n                    stopping a process.\",\n        alias = \"disable-advanced-kill\"\n    )]\n    pub disable_advanced_kill: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Prevents performing any actions that affect the system.\",\n        long_help = \"Prevents performing any actions that affect the system. Disables operations such as stopping or sending signals \\\n                 to processes.\",\n        alias = \"read-only\"\n    )]\n    pub read_only: bool,\n\n    #[cfg(target_os = \"linux\")]\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Hide kernel threads by default.\",\n        alias = \"hide-k-threads\"\n    )]\n    pub hide_k_threads: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Also gather process thread information.\",\n        alias = \"get-threads\",\n    )]\n    pub get_threads: bool,\n\n    #[arg(\n        short = 'g',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Groups processes with the same name by default when searching.\",\n        long_help = \"Groups processes with the same name by default when searching. Doesn't do anything if --tree is also set, or \\\n                    tree=true in the config.\",\n        alias = \"group-processes\"\n    )]\n    pub group_processes: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Defaults to showing process memory usage by value.\",\n        long_help = \"Defaults to showing process memory usage by value. Otherwise, it defaults to showing it by percentage.\",\n        alias = \"process-memory-as-value\"\n    )]\n    pub process_memory_as_value: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Shows the full command name instead of the process name by default.\",\n        alias = \"process-command\"\n    )]\n    pub process_command: bool,\n\n    #[arg(short = 'R', long, action = ArgAction::SetTrue, help = \"Enables regex by default while searching.\")]\n    pub regex: bool,\n\n    #[arg(\n        short = 'T',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Makes the process widget use tree mode by default.\"\n    )]\n    pub tree: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Collapse process tree by default.\",\n        alias = \"tree-collapse\"\n    )]\n    pub tree_collapse: bool,\n\n    #[arg(\n        short = 'n',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Show process CPU% usage without averaging over the number of CPU cores.\",\n        alias = \"unnormalized-cpu\"\n    )]\n    pub unnormalized_cpu: bool,\n\n    #[arg(\n        short = 'W',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Enables whole-word matching by default while searching.\",\n        alias = \"whole-word\"\n    )]\n    pub whole_word: bool,\n}\n\n/// Temperature arguments/config options.\n#[derive(Args, Clone, Debug, Default)]\n#[command(next_help_heading = \"Temperature Options\", rename_all = \"snake_case\")]\n#[group(id = \"temperature_unit\", multiple = false)]\npub struct TemperatureArgs {\n    #[arg(\n        short = 'c',\n        long,\n        action = ArgAction::SetTrue,\n        group = \"temperature_unit\",\n        help = \"Use Celsius as the temperature unit. Default.\",\n        long_help = \"Use Celsius as the temperature unit. This is the default option.\"\n    )]\n    pub celsius: bool,\n\n    #[arg(\n        short = 'f',\n        long,\n        action = ArgAction::SetTrue,\n        group = \"temperature_unit\",\n        help = \"Use Fahrenheit as the temperature unit.\"\n    )]\n    pub fahrenheit: bool,\n\n    #[arg(\n        short = 'k',\n        long,\n        action = ArgAction::SetTrue,\n        group = \"temperature_unit\",\n        help = \"Use Kelvin as the temperature unit.\"\n    )]\n    pub kelvin: bool,\n}\n\n/// The default selection of the CPU widget. If the given selection is invalid,\n/// we will fall back to all.\n#[derive(Clone, Copy, Debug, Default)]\npub enum CpuDefault {\n    #[default]\n    All,\n    Average,\n}\n\nimpl ValueEnum for CpuDefault {\n    fn value_variants<'a>() -> &'a [Self] {\n        &[CpuDefault::All, CpuDefault::Average]\n    }\n\n    fn to_possible_value(&self) -> Option<PossibleValue> {\n        match self {\n            CpuDefault::All => Some(PossibleValue::new(\"all\")),\n            CpuDefault::Average => Some(PossibleValue::new(\"avg\").alias(\"average\")),\n        }\n    }\n}\n\n/// CPU arguments/config options.\n#[derive(Args, Clone, Debug, Default)]\n#[command(next_help_heading = \"CPU Options\", rename_all = \"snake_case\")]\npub struct CpuArgs {\n    // TODO: Maybe rename this or fix this? Should this apply to all \"left legends\"?\n    #[arg(\n        short = 'l',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Puts the CPU chart legend on the left side.\",\n        alias = \"cpu-left-legend\"\n    )]\n    pub cpu_left_legend: bool,\n\n    #[arg(\n        long,\n        help = \"Sets which CPU entry type is selected by default.\",\n        value_name = \"ENTRY\",\n        value_parser = value_parser!(CpuDefault),\n        alias = \"default-cpu-entry\"\n    )]\n    pub default_cpu_entry: Option<CpuDefault>,\n\n    #[arg(\n        short = 'a',\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Hides the average CPU usage entry.\", \n        alias = \"hide-avg-cpu\"\n    )]\n    pub hide_avg_cpu: bool,\n}\n\n/// Memory argument/config options.\n#[derive(Args, Clone, Debug, Default)]\n#[command(next_help_heading = \"Memory Options\", rename_all = \"snake_case\")]\npub struct MemoryArgs {\n    #[arg(\n        long,\n        value_parser = CHART_WIDGET_POSITIONS,\n        value_name = \"POSITION\",\n        ignore_case = true,\n        help = \"Where to place the legend for the memory chart widget.\",\n        alias = \"memory-legend\"\n    )]\n    pub memory_legend: Option<String>,\n\n    #[cfg(not(target_os = \"windows\"))]\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Enables collecting and displaying cache and buffer memory.\",\n        alias = \"enable-cache-memory\"\n    )]\n    pub enable_cache_memory: bool,\n\n    #[cfg(feature = \"zfs\")]\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Subtract reclaimable ARC from memory.\",\n        alias = \"free-arc\"\n    )]\n    pub free_arc: bool,\n}\n\n/// Network arguments/config options.\n#[derive(Args, Clone, Debug, Default)]\n#[command(next_help_heading = \"Network Options\", rename_all = \"snake_case\")]\npub struct NetworkArgs {\n    #[arg(\n        long,\n        value_parser = CHART_WIDGET_POSITIONS,\n        value_name = \"POSITION\",\n        ignore_case = true,\n        help = \"Where to place the legend for the network chart widget.\",\n        alias = \"network-legend\"\n    )]\n    pub network_legend: Option<String>,\n\n    // TODO: Rename some of these to remove the network prefix for serde.\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Displays the network widget using bytes.\",\n        long_help = \"Displays the network widget using bytes. Defaults to bits.\",\n        alias = \"network-use-bytes\"\n    )]\n    pub network_use_bytes: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Displays the network widget with binary prefixes.\",\n        long_help = \"Displays the network widget with binary prefixes (e.g. kibibits, mebibits) rather than a decimal \\\n                    prefixes (e.g. kilobits, megabits). Defaults to decimal prefixes.\",\n        alias = \"network-use-binary-prefix\"\n    )]\n    pub network_use_binary_prefix: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Displays the network widget with a log scale.\",\n        long_help = \"Displays the network widget with a log scale. Defaults to a non-log scale.\",\n        alias = \"network-use-log\"\n    )]\n    pub network_use_log: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"(DEPRECATED) Uses a separate network legend.\",\n        long_help = \"(DEPRECATED) Uses separate network widget legend. This display is not tested and may be broken.\",\n        alias = \"use-old-network-legend\"\n    )]\n    pub use_old_network_legend: bool,\n\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Displays packets information (packet rate and average packet size) in the network widget.\",\n        long_help = \"Displays packets information including packet rate (packets per second) and average packet size in the network widget.\",\n        alias = \"show-packets\"\n    )]\n    pub show_packets: bool,\n}\n\n/// Battery arguments/config options.\n#[cfg(feature = \"battery\")]\n#[derive(Args, Clone, Debug, Default)]\n#[command(next_help_heading = \"Battery Options\", rename_all = \"snake_case\")]\npub struct BatteryArgs {\n    #[arg(\n        long,\n        action = ArgAction::SetTrue,\n        help = \"Shows the battery widget in non-custom layouts.\",\n        long_help = \"Shows the battery widget in default or basic mode, if there is as battery available. This \\\n                    has no effect on custom layouts; if the battery widget is desired for a custom layout, explicitly \\\n                    specify it.\"\n    )]\n    pub battery: bool,\n}\n\n/// GPU arguments/config options.\n#[cfg(feature = \"gpu\")]\n#[derive(Args, Clone, Debug, Default)]\n#[command(next_help_heading = \"GPU Options\", rename_all = \"snake_case\")]\npub struct GpuArgs {\n    #[arg(long, action = ArgAction::SetTrue, help = \"Disable collecting and displaying NVIDIA and AMD GPU information.\", alias = \"disable-gpu\")]\n    pub disable_gpu: bool,\n}\n\n/// Style arguments/config options.\n#[derive(Args, Clone, Debug, Default)]\n#[command(next_help_heading = \"Style Options\", rename_all = \"snake_case\")]\npub struct StyleArgs {\n    #[arg(\n        long,\n        value_name = \"SCHEME\",\n        value_parser = [\n            \"default\",\n            \"default-light\",\n            \"gruvbox\",\n            \"gruvbox-light\",\n            \"nord\",\n            \"nord-light\",\n        ],\n        hide_possible_values = true,\n        help = indoc! {\n            \"Use a built-in color theme, use '--help' for info on the colors. [possible values: default, default-light, gruvbox, gruvbox-light, nord, nord-light]\",\n        },\n        long_help = indoc! {\n            \"Use a pre-defined color theme. Currently supported themes are:\n            - default\n            - default-light (default but adjusted for lighter backgrounds)\n            - gruvbox       (a bright theme with 'retro groove' colors)\n            - gruvbox-light (gruvbox but adjusted for lighter backgrounds)\n            - nord          (an arctic, north-bluish color palette)\n            - nord-light    (nord but adjusted for lighter backgrounds)\"\n        }\n    )]\n    pub theme: Option<String>,\n}\n\n/// Other arguments. This just handle options that are for help/version\n/// displaying.\n#[derive(Args, Clone, Debug)]\n#[command(next_help_heading = \"Other Options\", rename_all = \"snake_case\")]\npub struct OtherArgs {\n    #[arg(short = 'h', long, action = ArgAction::Help, help = \"Prints help info (for more details use '--help'.\")]\n    help: (),\n\n    #[arg(short = 'V', long, action = ArgAction::Version, help = \"Prints version information.\")]\n    version: (),\n}\n\n/// Parse arguments and return a [`BottomArgs`]. If this fails it will exit the program.\npub fn get_args() -> BottomArgs {\n    BottomArgs::parse()\n}\n\n/// Returns an [`Command`] based off of [`BottomArgs`].\n#[cfg(test)]\npub(crate) fn build_cmd() -> Command {\n    BottomArgs::command()\n}\n\n#[cfg(test)]\nmod test {\n    use std::collections::HashSet;\n\n    use super::*;\n\n    #[test]\n    fn verify_cli() {\n        build_cmd().debug_assert();\n    }\n\n    #[test]\n    fn no_default_help_heading() {\n        let mut cmd = build_cmd();\n\n        let help_str = cmd.render_help();\n        assert!(\n            !help_str.to_string().contains(\"\\nOptions:\\n\"),\n            \"the default 'Options' heading should not exist; if it does then an argument is \\\n            missing a help heading.\"\n        );\n\n        let long_help_str = cmd.render_long_help();\n        assert!(\n            !long_help_str.to_string().contains(\"\\nOptions:\\n\"),\n            \"the default 'Options' heading should not exist; if it does then an argument is \\\n            missing a help heading.\"\n        );\n    }\n\n    #[test]\n    fn catch_incorrect_long_args() {\n        // Set this to allow certain ones through if needed.\n        let allow_list: HashSet<&str> = vec![].into_iter().collect();\n        let cmd = build_cmd();\n\n        for opt in cmd.get_opts() {\n            let long_flag = opt.get_long().unwrap();\n\n            if !allow_list.contains(long_flag) {\n                assert!(\n                    long_flag.len() < 30,\n                    \"the long help arg '{long_flag}' might be set wrong, please take a look!\"\n                );\n            }\n        }\n    }\n\n    #[test]\n    fn catch_missing_hyphen_alias() {\n        let cmd = build_cmd();\n\n        for opt in cmd.get_opts() {\n            let long_flag = opt.get_long().unwrap();\n            if long_flag.contains(\"_\") {\n                let aliased_version = long_flag.replace(\"_\", \"-\");\n                let stored_alias = opt.get_aliases().unwrap_or_else(|| {\n                    panic!(\"'{long_flag}' should have an alias, if not, it's missing\")\n                });\n\n                assert!(\n                    stored_alias.contains(&aliased_version.as_str()),\n                    \"'{long_flag}' has an incorrectly defined alias\"\n                );\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/options/config/cpu.rs",
    "content": "use serde::Deserialize;\n\n/// The default selected entry of the CPU widget.\n#[derive(Clone, Copy, Debug, Default, Deserialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[serde(rename_all = \"lowercase\")]\n#[cfg_attr(test, derive(PartialEq, Eq))]\npub(crate) enum CpuDefault {\n    #[default]\n    All,\n    #[serde(alias = \"avg\")]\n    Average,\n}\n\n/// CPU column settings.\n#[derive(Clone, Debug, Default, Deserialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct CpuConfig {\n    /// The default selected entry of the CPU widget.\n    #[serde(default)]\n    pub(crate) default: CpuDefault,\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn default_cpu_default() {\n        let config = \"\";\n        let generated: CpuConfig = toml_edit::de::from_str(config).unwrap();\n        match generated.default {\n            CpuDefault::All => {}\n            CpuDefault::Average => {\n                panic!(\"the default should be all\")\n            }\n        }\n    }\n\n    #[test]\n    fn all_cpu_default() {\n        let config = r#\"\n            default = \"all\"\n        \"#;\n        let generated: CpuConfig = toml_edit::de::from_str(config).unwrap();\n        match generated.default {\n            CpuDefault::All => {}\n            CpuDefault::Average => {\n                panic!(\"the default should be all\")\n            }\n        }\n    }\n\n    #[test]\n    fn avg_cpu_default() {\n        let config = r#\"\n            default = \"avg\"\n        \"#;\n\n        let generated: CpuConfig = toml_edit::de::from_str(config).unwrap();\n        match generated.default {\n            CpuDefault::All => {\n                panic!(\"the avg should be set\")\n            }\n            CpuDefault::Average => {}\n        }\n    }\n\n    #[test]\n    fn average_cpu_default() {\n        let config = r#\"\n            default = \"average\"\n        \"#;\n\n        let generated: CpuConfig = toml_edit::de::from_str(config).unwrap();\n        match generated.default {\n            CpuDefault::All => {\n                panic!(\"the avg should be set\")\n            }\n            CpuDefault::Average => {}\n        }\n    }\n}\n"
  },
  {
    "path": "src/options/config/disk.rs",
    "content": "use serde::Deserialize;\n\nuse super::IgnoreList;\nuse crate::options::DiskColumn;\n\n/// Disk configuration.\n#[derive(Clone, Debug, Default, Deserialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct DiskConfig {\n    /// A filter over the disk names.\n    pub(crate) name_filter: Option<IgnoreList>,\n\n    /// A filter over the mount names.\n    pub(crate) mount_filter: Option<IgnoreList>,\n\n    /// A list of disk widget columns.\n    #[serde(default)]\n    pub(crate) columns: Option<Vec<DiskColumn>>, // TODO: make this more composable(?) in the future, we might need to rethink how it's done for custom widgets\n}\n\n#[cfg(test)]\nmod test {\n    use super::DiskConfig;\n\n    #[test]\n    fn none_column_setting() {\n        let config = \"\";\n        let generated: DiskConfig = toml_edit::de::from_str(config).unwrap();\n        assert!(generated.columns.is_none());\n    }\n\n    #[test]\n    fn empty_column_setting() {\n        let config = r#\"columns = []\"#;\n        let generated: DiskConfig = toml_edit::de::from_str(config).unwrap();\n        assert!(generated.columns.unwrap().is_empty());\n    }\n\n    #[test]\n    fn valid_disk_column_settings() {\n        let config = r#\"columns = [\"disk\", \"mount\", \"used\", \"free\", \"total\", \"used%\", \"free%\", \"r/s\", \"w/s\"]\"#;\n        toml_edit::de::from_str::<DiskConfig>(config).expect(\"Should succeed!\");\n    }\n\n    #[test]\n    fn bad_disk_column_settings() {\n        let config = r#\"columns = [\"diskk\"]\"#;\n        toml_edit::de::from_str::<DiskConfig>(config).expect_err(\"Should error out!\");\n    }\n}\n"
  },
  {
    "path": "src/options/config/flags.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse super::StringOrNum;\n\n// TODO: Break this up.\n#[derive(Clone, Debug, Default, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct GeneralConfig {\n    pub(crate) hide_avg_cpu: Option<bool>,\n    pub(crate) dot_marker: Option<bool>,\n    pub(crate) temperature_type: Option<String>,\n    pub(crate) rate: Option<StringOrNum>,\n    pub(crate) cpu_left_legend: Option<bool>,\n    pub(crate) current_usage: Option<bool>,\n    pub(crate) unnormalized_cpu: Option<bool>,\n    pub(crate) group_processes: Option<bool>,\n    pub(crate) case_sensitive: Option<bool>,\n    pub(crate) whole_word: Option<bool>,\n    pub(crate) regex: Option<bool>,\n    pub(crate) basic: Option<bool>,\n    pub(crate) default_time_value: Option<StringOrNum>,\n    pub(crate) time_delta: Option<StringOrNum>,\n    pub(crate) autohide_time: Option<bool>,\n    pub(crate) hide_time: Option<bool>,\n    pub(crate) default_widget_type: Option<String>,\n    pub(crate) default_widget_count: Option<u64>,\n    pub(crate) expanded: Option<bool>,\n    pub(crate) use_old_network_legend: Option<bool>,\n    pub(crate) hide_table_gap: Option<bool>,\n    pub(crate) battery: Option<bool>,\n    pub(crate) disable_click: Option<bool>,\n    pub(crate) disable_keys: Option<bool>,\n    pub(crate) no_write: Option<bool>,\n    pub(crate) network_legend: Option<String>,\n    pub(crate) memory_legend: Option<String>,\n    pub(crate) process_memory_as_value: Option<bool>,\n    pub(crate) tree: Option<bool>,\n    pub(crate) show_table_scroll_position: Option<bool>,\n    pub(crate) process_command: Option<bool>,\n    // #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n    pub(crate) disable_advanced_kill: Option<bool>, // This does nothing on Windows, but we leave it enabled to make the config file consistent across platforms.\n    pub(crate) read_only: Option<bool>,\n    // #[cfg(target_os = \"linux\")]\n    pub(crate) hide_k_threads: Option<bool>,\n    // #[cfg(feature = \"zfs\")]\n    pub(crate) free_arc: Option<bool>,\n    pub(crate) network_use_bytes: Option<bool>,\n    pub(crate) network_use_log: Option<bool>,\n    pub(crate) network_use_binary_prefix: Option<bool>,\n    pub(crate) show_packets: Option<bool>,\n    pub(crate) disable_gpu: Option<bool>,\n    pub(crate) enable_cache_memory: Option<bool>,\n    pub(crate) retention: Option<StringOrNum>,\n    pub(crate) average_cpu_row: Option<bool>, // FIXME: This makes no sense outside of basic mode, add a basic mode config section.\n    pub(crate) tree_collapse: Option<bool>,\n}\n"
  },
  {
    "path": "src/options/config/ignore_list.rs",
    "content": "use serde::{Deserialize, Serialize};\n\n/// Workaround as per <https://github.com/serde-rs/serde/issues/1030>.\nfn default_as_true() -> bool {\n    true\n}\n\n#[derive(Clone, Debug, Default, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub struct IgnoreList {\n    #[serde(default = \"default_as_true\")]\n    // TODO: Deprecate and/or rename, current name sounds awful.\n    // Maybe to something like \"deny_entries\"?  Currently it defaults to a denylist anyways, so\n    // maybe \"allow_entries\"?\n    pub is_list_ignored: bool,\n    pub list: Vec<String>,\n    #[serde(default)]\n    pub regex: bool,\n    #[serde(default)]\n    pub case_sensitive: bool,\n    #[serde(default)]\n    pub whole_word: bool,\n}\n"
  },
  {
    "path": "src/options/config/layout.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse crate::{app::layout_manager::*, options::OptionResult};\n\n/// Represents a row. This has a length of some sort (optional) and a vector\n/// of children.\n#[derive(Clone, Deserialize, Debug, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\n#[serde(rename = \"row\")]\npub struct Row {\n    pub ratio: Option<u16>,\n    pub child: Option<Vec<RowChildren>>,\n}\n\nfn new_cpu(cpu_left_legend: bool, iter_id: &mut u64) -> BottomColRow {\n    let cpu_id = *iter_id;\n    *iter_id += 1;\n    let legend_id = *iter_id;\n\n    if cpu_left_legend {\n        BottomColRow::new(vec![\n            BottomWidget::new(BottomWidgetType::CpuLegend, legend_id)\n                .canvas_handled()\n                .with_ratio_override(3)\n                .parent_reflector(Some((WidgetDirection::Right, 1))),\n            BottomWidget::new(BottomWidgetType::Cpu, cpu_id).grow(Some(17)),\n        ])\n    } else {\n        BottomColRow::new(vec![\n            BottomWidget::new(BottomWidgetType::Cpu, cpu_id).grow(Some(17)),\n            BottomWidget::new(BottomWidgetType::CpuLegend, legend_id)\n                .canvas_handled()\n                .with_ratio_override(3)\n                .parent_reflector(Some((WidgetDirection::Left, 1))),\n        ])\n    }\n    .total_widget_ratio(20)\n}\n\nfn new_proc_sort(sort_id: u64) -> BottomWidget {\n    BottomWidget::new(BottomWidgetType::ProcSort, sort_id)\n        .canvas_handled()\n        .parent_reflector(Some((WidgetDirection::Right, 2)))\n}\n\nfn new_proc(proc_id: u64) -> BottomWidget {\n    BottomWidget::new(BottomWidgetType::Proc, proc_id).ratio(2)\n}\n\nfn new_proc_search(search_id: u64) -> BottomWidget {\n    BottomWidget::new(BottomWidgetType::ProcSearch, search_id)\n        .parent_reflector(Some((WidgetDirection::Up, 1)))\n}\n\nimpl Row {\n    pub fn convert_row_to_bottom_row(\n        &self, iter_id: &mut u64, total_height_ratio: &mut u16, default_widget_id: &mut u64,\n        default_widget_type: &Option<BottomWidgetType>, default_widget_count: &mut u64,\n        cpu_left_legend: bool,\n    ) -> OptionResult<BottomRow> {\n        // TODO: In the future we want to also add percentages.\n        // But for MVP, we aren't going to bother.\n        let row_ratio = self.ratio.unwrap_or(1);\n        let mut children = Vec::new();\n\n        *total_height_ratio += row_ratio;\n\n        let mut total_col_ratio = 0;\n        if let Some(row_children) = &self.child {\n            for row_child in row_children {\n                match row_child {\n                    RowChildren::Widget(widget) => {\n                        *iter_id += 1;\n                        let width_ratio = widget.ratio.unwrap_or(1);\n                        total_col_ratio += width_ratio;\n                        let widget_type = widget.widget_type.parse::<BottomWidgetType>()?;\n\n                        if let Some(default_widget_type_val) = default_widget_type {\n                            if *default_widget_type_val == widget_type && *default_widget_count > 0\n                            {\n                                *default_widget_count -= 1;\n                                if *default_widget_count == 0 {\n                                    *default_widget_id = *iter_id;\n                                }\n                            }\n                        } else {\n                            // Check default flag\n                            if let Some(default_widget_flag) = widget.default {\n                                if default_widget_flag {\n                                    *default_widget_id = *iter_id;\n                                }\n                            }\n                        }\n\n                        children.push(match widget_type {\n                            BottomWidgetType::Cpu => {\n                                BottomCol::new(vec![new_cpu(cpu_left_legend, iter_id)])\n                                    .ratio(width_ratio)\n                            }\n                            BottomWidgetType::Proc => {\n                                let proc_id = *iter_id;\n                                let proc_search_id = *iter_id + 1;\n                                *iter_id += 2;\n                                BottomCol::new(vec![\n                                    BottomColRow::new(vec![\n                                        new_proc_sort(*iter_id),\n                                        new_proc(proc_id),\n                                    ])\n                                    .grow(None)\n                                    .total_widget_ratio(3),\n                                    BottomColRow::new(vec![new_proc_search(proc_search_id)])\n                                        .canvas_handled(),\n                                ])\n                                .total_col_row_ratio(2)\n                                .ratio(width_ratio)\n                            }\n                            _ => BottomCol::new(vec![BottomColRow::new(vec![BottomWidget::new(\n                                widget_type,\n                                *iter_id,\n                            )])])\n                            .ratio(width_ratio),\n                        });\n                    }\n                    RowChildren::Col { ratio, child } => {\n                        let col_width_ratio = ratio.unwrap_or(1);\n                        total_col_ratio += col_width_ratio;\n                        let mut total_col_row_ratio = 0;\n\n                        let mut col_row_children: Vec<BottomColRow> = Vec::new();\n\n                        for widget in child {\n                            let widget_type = widget.widget_type.parse::<BottomWidgetType>()?;\n                            *iter_id += 1;\n\n                            if let Some(default_widget_type_val) = default_widget_type {\n                                if *default_widget_type_val == widget_type\n                                    && *default_widget_count > 0\n                                {\n                                    *default_widget_count -= 1;\n                                    if *default_widget_count == 0 {\n                                        *default_widget_id = *iter_id;\n                                    }\n                                }\n                            } else {\n                                // Check default flag\n                                if let Some(default_widget_flag) = widget.default {\n                                    if default_widget_flag {\n                                        *default_widget_id = *iter_id;\n                                    }\n                                }\n                            }\n\n                            match widget_type {\n                                BottomWidgetType::Cpu => {\n                                    let col_row_height_ratio = widget.ratio.unwrap_or(1);\n                                    total_col_row_ratio += col_row_height_ratio;\n\n                                    col_row_children.push(\n                                        new_cpu(cpu_left_legend, iter_id)\n                                            .ratio(col_row_height_ratio),\n                                    );\n                                }\n                                BottomWidgetType::Proc => {\n                                    let col_row_height_ratio = widget.ratio.unwrap_or(1) + 1;\n                                    total_col_row_ratio += col_row_height_ratio;\n\n                                    let proc_id = *iter_id;\n                                    let proc_search_id = *iter_id + 1;\n                                    *iter_id += 2;\n                                    col_row_children.push(\n                                        BottomColRow::new(vec![\n                                            new_proc_sort(*iter_id),\n                                            new_proc(proc_id),\n                                        ])\n                                        .ratio(col_row_height_ratio)\n                                        .total_widget_ratio(3),\n                                    );\n                                    col_row_children.push(\n                                        BottomColRow::new(vec![new_proc_search(proc_search_id)])\n                                            .canvas_handled(),\n                                    );\n                                }\n                                _ => {\n                                    let col_row_height_ratio = widget.ratio.unwrap_or(1);\n                                    total_col_row_ratio += col_row_height_ratio;\n\n                                    col_row_children.push(\n                                        BottomColRow::new(vec![BottomWidget::new(\n                                            widget_type,\n                                            *iter_id,\n                                        )])\n                                        .ratio(col_row_height_ratio),\n                                    )\n                                }\n                            }\n                        }\n\n                        children.push(\n                            BottomCol::new(col_row_children)\n                                .total_col_row_ratio(total_col_row_ratio)\n                                .ratio(col_width_ratio),\n                        );\n                    }\n                }\n            }\n        }\n\n        Ok(BottomRow::new(children)\n            .total_col_ratio(total_col_ratio)\n            .ratio(row_ratio))\n    }\n}\n\n/// Represents a child of a Row - either a Col (column) or a FinalWidget.\n///\n/// A Col can also have an optional length and children.  We only allow columns\n/// to have FinalWidgets as children, lest we get some amount of mutual\n/// recursion between Row and Col.\n#[derive(Clone, Deserialize, Debug, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[serde(untagged)]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub enum RowChildren {\n    Widget(FinalWidget),\n    Col {\n        ratio: Option<u16>,\n        child: Vec<FinalWidget>,\n    },\n}\n\n/// Represents a widget.\n#[derive(Clone, Deserialize, Debug, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub struct FinalWidget {\n    pub ratio: Option<u16>,\n    #[serde(rename = \"type\")]\n    pub widget_type: String,\n    pub default: Option<bool>,\n}\n\n#[cfg(test)]\nmod test {\n    use toml_edit::de::from_str;\n\n    use super::*;\n    use crate::{\n        constants::{DEFAULT_LAYOUT, DEFAULT_WIDGET_ID},\n        options::Config,\n    };\n\n    const PROC_LAYOUT: &str = r#\"\n    [[row]]\n        [[row.child]]\n            type=\"proc\"\n    [[row]]\n        [[row.child]]\n            type=\"proc\"\n        [[row.child]]\n            type=\"proc\"\n    [[row]]\n        [[row.child]]\n            type=\"proc\"\n        [[row.child]]\n            type=\"proc\"\n    \"#;\n\n    fn test_create_layout(\n        rows: &[Row], default_widget_id: u64, default_widget_type: Option<BottomWidgetType>,\n        default_widget_count: u64, left_legend: bool,\n    ) -> BottomLayout {\n        let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs*\n        let mut total_height_ratio = 0;\n        let mut default_widget_count = default_widget_count;\n        let mut default_widget_id = default_widget_id;\n\n        let mut ret_bottom_layout = BottomLayout {\n            rows: rows\n                .iter()\n                .map(|row| {\n                    row.convert_row_to_bottom_row(\n                        &mut iter_id,\n                        &mut total_height_ratio,\n                        &mut default_widget_id,\n                        &default_widget_type,\n                        &mut default_widget_count,\n                        left_legend,\n                    )\n                })\n                .collect::<OptionResult<Vec<_>>>()\n                .unwrap(),\n            total_row_height_ratio: total_height_ratio,\n        };\n        ret_bottom_layout.get_movement_mappings();\n\n        ret_bottom_layout\n    }\n\n    #[test]\n    /// Tests the default setup.\n    fn test_default_movement() {\n        let rows = from_str::<Config>(DEFAULT_LAYOUT).unwrap().row.unwrap();\n        let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, false);\n\n        // Simple tests for the top CPU widget\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].down_neighbour,\n            Some(3)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].right_neighbour,\n            Some(2)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].left_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].up_neighbour,\n            None\n        );\n\n        // Test CPU legend\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].down_neighbour,\n            Some(4)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].right_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].left_neighbour,\n            Some(1)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].up_neighbour,\n            None\n        );\n\n        // Test memory->temp, temp->disk, disk->memory mappings\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[0].children[0].right_neighbour,\n            Some(4)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[1].children[0].children[0].down_neighbour,\n            Some(5)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[1].children[1].children[0].left_neighbour,\n            Some(3)\n        );\n\n        // Test disk -> processes, processes -> process sort, process sort -> network\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[1].children[1].children[0].down_neighbour,\n            Some(7)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[0].children[1].left_neighbour,\n            Some(9)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[0].children[0].left_neighbour,\n            Some(6)\n        );\n    }\n\n    #[cfg(feature = \"battery\")]\n    #[test]\n    /// Tests battery movement in the default setup.\n    fn test_default_battery_movement() {\n        use crate::constants::DEFAULT_BATTERY_LAYOUT;\n\n        let rows = from_str::<Config>(DEFAULT_BATTERY_LAYOUT)\n            .unwrap()\n            .row\n            .unwrap();\n        let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, false);\n\n        // Simple tests for the top CPU widget\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].down_neighbour,\n            Some(4)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].right_neighbour,\n            Some(2)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].left_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].up_neighbour,\n            None\n        );\n\n        // Test CPU legend\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].down_neighbour,\n            Some(5)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].right_neighbour,\n            Some(3)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].left_neighbour,\n            Some(1)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].up_neighbour,\n            None\n        );\n    }\n\n    #[test]\n    /// Tests using cpu_left_legend.\n    fn test_cpu_left_legend() {\n        let rows = from_str::<Config>(DEFAULT_LAYOUT).unwrap().row.unwrap();\n        let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, true);\n\n        // Legend\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].down_neighbour,\n            Some(3)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].right_neighbour,\n            Some(1)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].left_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].up_neighbour,\n            None\n        );\n\n        // Widget\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].down_neighbour,\n            Some(3)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].right_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].left_neighbour,\n            Some(2)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].up_neighbour,\n            None\n        );\n    }\n\n    #[test]\n    /// Tests explicit default widget.\n    fn test_default_widget_in_layout() {\n        let proc_layout = r#\"\n    [[row]]\n        [[row.child]]\n            type=\"proc\"\n    [[row]]\n        [[row.child]]\n            type=\"proc\"\n        [[row.child]]\n            type=\"proc\"\n    [[row]]\n        [[row.child]]\n            type=\"proc\"\n            default=true\n        [[row.child]]\n            type=\"proc\"\n    \"#;\n\n        let rows = from_str::<Config>(proc_layout).unwrap().row.unwrap();\n        let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs*\n        let mut total_height_ratio = 0;\n        let mut default_widget_count = 1;\n        let mut default_widget_id = DEFAULT_WIDGET_ID;\n        let default_widget_type = None;\n        let cpu_left_legend = false;\n\n        let mut ret_bottom_layout = BottomLayout {\n            rows: rows\n                .iter()\n                .map(|row| {\n                    row.convert_row_to_bottom_row(\n                        &mut iter_id,\n                        &mut total_height_ratio,\n                        &mut default_widget_id,\n                        &default_widget_type,\n                        &mut default_widget_count,\n                        cpu_left_legend,\n                    )\n                })\n                .collect::<OptionResult<Vec<_>>>()\n                .unwrap(),\n            total_row_height_ratio: total_height_ratio,\n        };\n        ret_bottom_layout.get_movement_mappings();\n\n        assert_eq!(default_widget_id, 10);\n    }\n\n    #[test]\n    /// Tests default widget by setting type and count.\n    fn test_default_widget_by_option() {\n        let rows = from_str::<Config>(PROC_LAYOUT).unwrap().row.unwrap();\n        let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs*\n        let mut total_height_ratio = 0;\n        let mut default_widget_count = 3;\n        let mut default_widget_id = DEFAULT_WIDGET_ID;\n        let default_widget_type = Some(BottomWidgetType::Proc);\n        let cpu_left_legend = false;\n\n        let mut ret_bottom_layout = BottomLayout {\n            rows: rows\n                .iter()\n                .map(|row| {\n                    row.convert_row_to_bottom_row(\n                        &mut iter_id,\n                        &mut total_height_ratio,\n                        &mut default_widget_id,\n                        &default_widget_type,\n                        &mut default_widget_count,\n                        cpu_left_legend,\n                    )\n                })\n                .collect::<OptionResult<Vec<_>>>()\n                .unwrap(),\n            total_row_height_ratio: total_height_ratio,\n        };\n        ret_bottom_layout.get_movement_mappings();\n\n        assert_eq!(default_widget_id, 7);\n    }\n\n    #[test]\n    fn test_proc_custom_layout() {\n        let rows = from_str::<Config>(PROC_LAYOUT).unwrap().row.unwrap();\n        let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, false);\n\n        // First proc widget\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].down_neighbour,\n            Some(2)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].left_neighbour,\n            Some(3)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].right_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[1].up_neighbour,\n            None\n        );\n\n        // Its search\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[1].children[0].down_neighbour,\n            Some(4)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[1].children[0].left_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[1].children[0].right_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[1].children[0].up_neighbour,\n            Some(1)\n        );\n\n        // Its sort\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].down_neighbour,\n            Some(2)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].left_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].right_neighbour,\n            Some(1)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[0].children[0].children[0].children[0].up_neighbour,\n            None\n        );\n\n        // Let us now test the second row's first widget...\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[0].children[1].down_neighbour,\n            Some(5)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[0].children[1].left_neighbour,\n            Some(6)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[0].children[1].right_neighbour,\n            Some(9)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[0].children[1].up_neighbour,\n            Some(2)\n        );\n\n        // Sort\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[0].children[0].down_neighbour,\n            Some(5)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[0].children[0].left_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[0].children[0].right_neighbour,\n            Some(4)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[0].children[0].up_neighbour,\n            Some(2)\n        );\n\n        // Search\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[1].children[0].down_neighbour,\n            Some(10)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[1].children[0].left_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[1].children[0].right_neighbour,\n            Some(8)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[1].children[0].children[1].children[0].up_neighbour,\n            Some(4)\n        );\n\n        // Third row, second\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[0].children[1].down_neighbour,\n            Some(14)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[0].children[1].left_neighbour,\n            Some(15)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[0].children[1].right_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[0].children[1].up_neighbour,\n            Some(8)\n        );\n\n        // Sort\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[0].children[0].down_neighbour,\n            Some(14)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[0].children[0].left_neighbour,\n            Some(10)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[0].children[0].right_neighbour,\n            Some(13)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[0].children[0].up_neighbour,\n            Some(8)\n        );\n\n        // Search\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[1].children[0].down_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[1].children[0].left_neighbour,\n            Some(11)\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[1].children[0].right_neighbour,\n            None\n        );\n        assert_eq!(\n            ret_bottom_layout.rows[2].children[1].children[1].children[0].up_neighbour,\n            Some(13)\n        );\n    }\n}\n"
  },
  {
    "path": "src/options/config/network.rs",
    "content": "use serde::Deserialize;\n\nuse super::IgnoreList;\n\n/// Network configuration.\n#[derive(Clone, Debug, Default, Deserialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct NetworkConfig {\n    /// A filter over the network interface names.\n    pub(crate) interface_filter: Option<IgnoreList>,\n    /// Whether to show packets information (packet rate and average packet size).\n    pub(crate) show_packets: Option<bool>,\n}\n"
  },
  {
    "path": "src/options/config/process.rs",
    "content": "use serde::Deserialize;\n\nuse crate::widgets::ProcColumn;\n\n/// Process configuration.\n#[derive(Clone, Debug, Default, Deserialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct ProcessesConfig {\n    /// A list of process widget columns.\n    #[serde(default)]\n    pub columns: Vec<ProcColumn>, // TODO: make this more composable(?) in the future, we might need to rethink how it's done for custom widgets\n\n    /// Whether to get process child threads.\n    pub get_threads: Option<bool>,\n}\n\n#[cfg(test)]\nmod test {\n    use super::{ProcColumn, ProcessesConfig};\n    use crate::widgets::ProcWidgetColumn;\n\n    #[test]\n    fn empty_column_setting() {\n        let config = \"\";\n        let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();\n        assert!(generated.columns.is_empty());\n    }\n\n    fn to_columns(columns: Vec<ProcColumn>) -> Vec<ProcWidgetColumn> {\n        columns\n            .iter()\n            .map(ProcWidgetColumn::from)\n            .collect::<Vec<_>>()\n    }\n\n    #[test]\n    fn valid_process_column_config() {\n        #[cfg(unix)]\n        let config = r#\"\n            columns = [\"CPU%\", \"PiD\", \"user\", \"MEM\", \"virt\", \"Tread\", \"T.Write\", \"Rps\", \"W/s\", \"tiMe\", \"USER\", \"state\", \"prioRity\", \"Nice\"]\n        \"#;\n\n        #[cfg(target_os = \"windows\")]\n        let config = r#\"\n            columns = [\"CPU%\", \"PiD\", \"user\", \"MEM\", \"virt\", \"Tread\", \"T.Write\", \"Rps\", \"W/s\", \"tiMe\", \"USER\", \"state\", \"prioRity\"]\n        \"#;\n\n        let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();\n        assert_eq!(\n            to_columns(generated.columns),\n            vec![\n                ProcWidgetColumn::Cpu,\n                ProcWidgetColumn::PidOrCount,\n                ProcWidgetColumn::User,\n                ProcWidgetColumn::Mem,\n                ProcWidgetColumn::VirtualMem,\n                ProcWidgetColumn::TotalRead,\n                ProcWidgetColumn::TotalWrite,\n                ProcWidgetColumn::ReadPerSecond,\n                ProcWidgetColumn::WritePerSecond,\n                ProcWidgetColumn::Time,\n                ProcWidgetColumn::User,\n                ProcWidgetColumn::State,\n                ProcWidgetColumn::Priority,\n                #[cfg(unix)]\n                ProcWidgetColumn::Nice,\n            ],\n        );\n    }\n\n    #[test]\n    fn bad_process_column_config() {\n        let config = r#\"columns = [\"MEM\", \"TWrite\", \"Cpuz\", \"read\", \"wps\"]\"#;\n        toml_edit::de::from_str::<ProcessesConfig>(config).expect_err(\"Should error out!\");\n    }\n\n    #[test]\n    fn valid_process_column_config_2() {\n        let config = r#\"columns = [\"Twrite\", \"T.Write\"]\"#;\n        let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();\n        assert_eq!(\n            to_columns(generated.columns),\n            vec![ProcWidgetColumn::TotalWrite; 2]\n        );\n\n        let config = r#\"columns = [\"Tread\", \"T.read\"]\"#;\n        let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();\n        assert_eq!(\n            to_columns(generated.columns),\n            vec![ProcWidgetColumn::TotalRead; 2]\n        );\n\n        let config = r#\"columns = [\"read\", \"rps\", \"r/s\"]\"#;\n        let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();\n        assert_eq!(\n            to_columns(generated.columns),\n            vec![ProcWidgetColumn::ReadPerSecond; 3]\n        );\n\n        let config = r#\"columns = [\"write\", \"wps\", \"w/s\"]\"#;\n        let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();\n        assert_eq!(\n            to_columns(generated.columns),\n            vec![ProcWidgetColumn::WritePerSecond; 3]\n        );\n    }\n}\n"
  },
  {
    "path": "src/options/config/style/battery.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse super::ColorStr;\n\n/// Styling specific to the battery widget.\n#[derive(Clone, Debug, Default, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct BatteryStyle {\n    /// The colour of the battery widget bar when the battery is over 50%.\n    #[serde(alias = \"high_battery_colour\")]\n    pub(crate) high_battery_color: Option<ColorStr>,\n\n    /// The colour of the battery widget bar when the battery between 10% to 50%.\n    #[serde(alias = \"medium_battery_colour\")]\n    pub(crate) medium_battery_color: Option<ColorStr>,\n\n    /// The colour of the battery widget bar when the battery is under 10%.\n    #[serde(alias = \"low_battery_colour\")]\n    pub(crate) low_battery_color: Option<ColorStr>,\n}\n"
  },
  {
    "path": "src/options/config/style/borders.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse tui::widgets::BorderType;\n\n#[derive(Default, Clone, Copy, Debug, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, derive(PartialEq, Eq))]\npub(crate) enum WidgetBorderType {\n    #[default]\n    Default,\n    Rounded,\n    Double,\n    Thick,\n}\n\nimpl<'de> Deserialize<'de> for WidgetBorderType {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: serde::Deserializer<'de>,\n    {\n        let value = String::deserialize(deserializer)?.to_lowercase();\n        match value.as_str() {\n            \"default\" => Ok(WidgetBorderType::Default),\n            \"rounded\" => Ok(WidgetBorderType::Rounded),\n            \"double\" => Ok(WidgetBorderType::Double),\n            \"thick\" => Ok(WidgetBorderType::Thick),\n            _ => Err(serde::de::Error::custom(\n                \"doesn't match any widget border type\",\n            )),\n        }\n    }\n}\n\nimpl From<WidgetBorderType> for BorderType {\n    fn from(value: WidgetBorderType) -> Self {\n        match value {\n            WidgetBorderType::Default => BorderType::Plain,\n            WidgetBorderType::Rounded => BorderType::Rounded,\n            WidgetBorderType::Double => BorderType::Double,\n            WidgetBorderType::Thick => BorderType::Thick,\n        }\n    }\n}\n"
  },
  {
    "path": "src/options/config/style/cpu.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse super::ColorStr;\n\n/// Styling specific to the CPU widget.\n#[derive(Clone, Debug, Default, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct CpuStyle {\n    /// The colour of the \"All\" CPU label.\n    #[serde(alias = \"all_entry_colour\")]\n    pub(crate) all_entry_color: Option<ColorStr>,\n\n    /// The colour of the average CPU label and graph line.\n    #[serde(alias = \"avg_entry_colour\")]\n    pub(crate) avg_entry_color: Option<ColorStr>,\n\n    /// Colour of each CPU threads' label and graph line. Read in order.\n    #[serde(alias = \"cpu_core_colours\")]\n    pub(crate) cpu_core_colors: Option<Vec<ColorStr>>,\n}\n"
  },
  {
    "path": "src/options/config/style/graphs.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse super::{ColorStr, TextStyleConfig};\n\n/// General styling for graph widgets.\n#[derive(Clone, Debug, Default, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct GraphStyle {\n    /// The general colour of the parts of the graph.\n    #[serde(alias = \"graph_colour\")]\n    pub(crate) graph_color: Option<ColorStr>,\n\n    /// Text styling for graph's legend text.\n    pub(crate) legend_text: Option<TextStyleConfig>,\n}\n"
  },
  {
    "path": "src/options/config/style/memory.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse super::ColorStr;\n// TODO: Maybe I should swap the alias and the field name since internally I use u.\n\n/// Styling specific to the memory widget.\n#[derive(Clone, Debug, Default, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct MemoryStyle {\n    /// The colour of the RAM label and graph line.\n    #[serde(alias = \"ram_colour\")]\n    pub(crate) ram_color: Option<ColorStr>,\n\n    /// The colour of the cache label and graph line. Does not do anything on Windows.\n    #[cfg_attr(target_os = \"windows\", allow(dead_code))]\n    #[serde(alias = \"cache_colour\")]\n    pub(crate) cache_color: Option<ColorStr>,\n\n    /// The colour of the swap label and graph line.\n    #[serde(alias = \"swap_colour\")]\n    pub(crate) swap_color: Option<ColorStr>,\n\n    /// The colour of the ARC label and graph line.\n    #[serde(alias = \"arc_colour\")]\n    pub(crate) arc_color: Option<ColorStr>,\n\n    /// Colour of each GPU's memory label and graph line. Read in order.\n    #[serde(alias = \"gpu_colours\")]\n    pub(crate) gpu_colors: Option<Vec<ColorStr>>,\n}\n"
  },
  {
    "path": "src/options/config/style/network.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse super::ColorStr;\n\n/// Styling specific to the network widget.\n#[derive(Clone, Debug, Default, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct NetworkStyle {\n    /// The colour of the RX (download) label and graph line.\n    #[serde(alias = \"rx_colour\")]\n    pub(crate) rx_color: Option<ColorStr>,\n\n    /// The colour of the TX (upload) label and graph line.\n    #[serde(alias = \"tx_colour\")]\n    pub(crate) tx_color: Option<ColorStr>,\n\n    /// he colour of the total RX (download) label in basic mode.\n    #[serde(alias = \"rx_total_colour\")]\n    pub(crate) rx_total_color: Option<ColorStr>,\n\n    /// The colour of the total TX (upload) label in basic mode.\n    #[serde(alias = \"tx_total_colour\")]\n    pub(crate) tx_total_color: Option<ColorStr>,\n}\n"
  },
  {
    "path": "src/options/config/style/tables.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse super::TextStyleConfig;\n\n/// General styling for table widgets.\n#[derive(Clone, Debug, Default, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct TableStyle {\n    /// Text styling for table headers.\n    pub(crate) headers: Option<TextStyleConfig>,\n}\n"
  },
  {
    "path": "src/options/config/style/themes/default.rs",
    "content": "use tui::{\n    style::{Color, Modifier, Style},\n    widgets::BorderType,\n};\n\nuse super::color;\nuse crate::options::config::style::Styles;\n\nimpl Styles {\n    pub(crate) fn default_palette() -> Self {\n        const FIRST_COLOUR: Color = Color::LightMagenta;\n        const SECOND_COLOUR: Color = Color::LightYellow;\n        const THIRD_COLOUR: Color = Color::LightCyan;\n        const FOURTH_COLOUR: Color = Color::LightGreen;\n        #[cfg(not(target_os = \"windows\"))]\n        const FIFTH_COLOUR: Color = Color::LightRed;\n        const HIGHLIGHT_COLOUR: Color = Color::LightBlue;\n        const AVG_COLOUR: Color = Color::Red;\n        const ALL_COLOUR: Color = Color::Green;\n        const DEFAULT_SELECTED_TEXT_STYLE: Style = color!(Color::Black).bg(HIGHLIGHT_COLOUR);\n        const TEXT_COLOUR: Color = Color::Gray;\n\n        Self {\n            ram_style: color!(FIRST_COLOUR),\n            #[cfg(not(target_os = \"windows\"))]\n            cache_style: color!(FIFTH_COLOUR),\n            swap_style: color!(SECOND_COLOUR),\n            #[cfg(feature = \"zfs\")]\n            arc_style: color!(THIRD_COLOUR),\n            #[cfg(feature = \"gpu\")]\n            gpu_colours: vec![\n                color!(FOURTH_COLOUR),\n                color!(Color::LightBlue),\n                color!(Color::LightRed),\n                color!(Color::Cyan),\n                color!(Color::Green),\n                color!(Color::Blue),\n                color!(Color::Red),\n            ],\n            rx_style: color!(FIRST_COLOUR),\n            tx_style: color!(SECOND_COLOUR),\n            total_rx_style: color!(THIRD_COLOUR),\n            total_tx_style: color!(FOURTH_COLOUR),\n            all_cpu_colour: color!(ALL_COLOUR),\n            avg_cpu_colour: color!(AVG_COLOUR),\n            cpu_colour_styles: vec![\n                color!(Color::LightMagenta),\n                color!(Color::LightYellow),\n                color!(Color::LightCyan),\n                color!(Color::LightGreen),\n                color!(Color::LightBlue),\n                color!(Color::Cyan),\n                color!(Color::Green),\n                color!(Color::Blue),\n            ],\n            border_style: color!(TEXT_COLOUR),\n            highlighted_border_style: color!(HIGHLIGHT_COLOUR),\n            text_style: color!(TEXT_COLOUR),\n            selected_text_style: DEFAULT_SELECTED_TEXT_STYLE,\n            table_header_style: color!(HIGHLIGHT_COLOUR).add_modifier(Modifier::BOLD),\n            widget_title_style: color!(TEXT_COLOUR),\n            graph_style: color!(TEXT_COLOUR),\n            graph_legend_style: color!(TEXT_COLOUR),\n            high_battery: color!(Color::Green),\n            medium_battery: color!(Color::Yellow),\n            low_battery: color!(Color::Red),\n            invalid_query_style: color!(Color::Red),\n            disabled_text_style: color!(Color::DarkGray),\n            border_type: BorderType::Plain,\n            #[cfg(target_os = \"linux\")]\n            thread_text_style: color!(Color::Green),\n        }\n    }\n\n    pub fn default_light_palette() -> Self {\n        Self {\n            ram_style: color!(Color::Blue),\n            #[cfg(not(target_os = \"windows\"))]\n            cache_style: color!(Color::LightRed),\n            swap_style: color!(Color::Red),\n            #[cfg(feature = \"zfs\")]\n            arc_style: color!(Color::LightBlue),\n            #[cfg(feature = \"gpu\")]\n            gpu_colours: vec![\n                color!(Color::LightGreen),\n                color!(Color::LightCyan),\n                color!(Color::LightRed),\n                color!(Color::Cyan),\n                color!(Color::Green),\n                color!(Color::Blue),\n                color!(Color::Red),\n            ],\n            rx_style: color!(Color::Blue),\n            tx_style: color!(Color::Red),\n            total_rx_style: color!(Color::LightBlue),\n            total_tx_style: color!(Color::LightRed),\n            cpu_colour_styles: vec![\n                color!(Color::LightMagenta),\n                color!(Color::LightBlue),\n                color!(Color::LightRed),\n                color!(Color::Cyan),\n                color!(Color::Green),\n                color!(Color::Blue),\n                color!(Color::Red),\n            ],\n            border_style: color!(Color::Black),\n            text_style: color!(Color::Black),\n            selected_text_style: color!(Color::White).bg(Color::LightBlue),\n            table_header_style: color!(Color::Black).add_modifier(Modifier::BOLD),\n            widget_title_style: color!(Color::Black),\n            graph_style: color!(Color::Black),\n            graph_legend_style: color!(Color::Black),\n            disabled_text_style: color!(Color::Gray),\n            ..Self::default_palette()\n        }\n    }\n}\n\nmod tests {\n    #[test]\n    fn default_palettes_valid() {\n        let _ = super::Styles::default_palette();\n        let _ = super::Styles::default_light_palette();\n    }\n}\n"
  },
  {
    "path": "src/options/config/style/themes/gruvbox.rs",
    "content": "use tui::{\n    style::{Color, Modifier},\n    widgets::BorderType,\n};\n\nuse super::{color, hex};\nuse crate::options::config::style::{Styles, themes::hex_colour};\n\nimpl Styles {\n    pub(crate) fn gruvbox_palette() -> Self {\n        Self {\n            ram_style: hex!(\"#8ec07c\"),\n            #[cfg(not(target_os = \"windows\"))]\n            cache_style: hex!(\"#b16286\"),\n            swap_style: hex!(\"#fabd2f\"),\n            #[cfg(feature = \"zfs\")]\n            arc_style: hex!(\"#689d6a\"),\n            #[cfg(feature = \"gpu\")]\n            gpu_colours: vec![\n                hex!(\"#d79921\"),\n                hex!(\"#458588\"),\n                hex!(\"#b16286\"),\n                hex!(\"#fe8019\"),\n                hex!(\"#b8bb26\"),\n                hex!(\"#cc241d\"),\n                hex!(\"#98971a\"),\n            ],\n            rx_style: hex!(\"#8ec07c\"),\n            tx_style: hex!(\"#fabd2f\"),\n            total_rx_style: hex!(\"#689d6a\"),\n            total_tx_style: hex!(\"#d79921\"),\n            all_cpu_colour: hex!(\"#8ec07c\"),\n            avg_cpu_colour: hex!(\"#fb4934\"),\n            cpu_colour_styles: vec![\n                hex!(\"#cc241d\"),\n                hex!(\"#98971a\"),\n                hex!(\"#d79921\"),\n                hex!(\"#458588\"),\n                hex!(\"#b16286\"),\n                hex!(\"#689d6a\"),\n                hex!(\"#fe8019\"),\n                hex!(\"#b8bb26\"),\n                hex!(\"#fabd2f\"),\n                hex!(\"#83a598\"),\n                hex!(\"#d3869b\"),\n                hex!(\"#d65d0e\"),\n                hex!(\"#9d0006\"),\n                hex!(\"#79740e\"),\n                hex!(\"#b57614\"),\n                hex!(\"#076678\"),\n                hex!(\"#8f3f71\"),\n                hex!(\"#427b58\"),\n                hex!(\"#d65d03\"),\n                hex!(\"#af3a03\"),\n            ],\n            border_style: hex!(\"#ebdbb2\"),\n            highlighted_border_style: hex!(\"#fe8019\"),\n            text_style: hex!(\"#ebdbb2\"),\n            selected_text_style: hex!(\"#1d2021\").bg(hex_colour!(\"#ebdbb2\")),\n            table_header_style: hex!(\"#83a598\").add_modifier(Modifier::BOLD),\n            widget_title_style: hex!(\"#ebdbb2\"),\n            graph_style: hex!(\"#ebdbb2\"),\n            graph_legend_style: hex!(\"#ebdbb2\"),\n            high_battery: hex!(\"#98971a\"),\n            medium_battery: hex!(\"#fabd2f\"),\n            low_battery: hex!(\"#fb4934\"),\n            invalid_query_style: color!(Color::Red),\n            disabled_text_style: hex!(\"#665c54\"),\n            border_type: BorderType::Plain,\n            #[cfg(target_os = \"linux\")]\n            thread_text_style: hex!(\"#458588\"),\n        }\n    }\n\n    pub(crate) fn gruvbox_light_palette() -> Self {\n        Self {\n            ram_style: hex!(\"#427b58\"),\n            #[cfg(not(target_os = \"windows\"))]\n            cache_style: hex!(\"#d79921\"),\n            swap_style: hex!(\"#cc241d\"),\n            #[cfg(feature = \"zfs\")]\n            arc_style: hex!(\"#689d6a\"),\n            #[cfg(feature = \"gpu\")]\n            gpu_colours: vec![\n                hex!(\"#9d0006\"),\n                hex!(\"#98971a\"),\n                hex!(\"#d79921\"),\n                hex!(\"#458588\"),\n                hex!(\"#b16286\"),\n                hex!(\"#fe8019\"),\n                hex!(\"#b8bb26\"),\n            ],\n            rx_style: hex!(\"#427b58\"),\n            tx_style: hex!(\"#cc241d\"),\n            total_rx_style: hex!(\"#689d6a\"),\n            total_tx_style: hex!(\"#d79921\"),\n            all_cpu_colour: hex!(\"#8ec07c\"),\n            avg_cpu_colour: hex!(\"#fb4934\"),\n            cpu_colour_styles: vec![\n                hex!(\"#cc241d\"),\n                hex!(\"#98971a\"),\n                hex!(\"#d79921\"),\n                hex!(\"#458588\"),\n                hex!(\"#b16286\"),\n                hex!(\"#689d6a\"),\n                hex!(\"#fe8019\"),\n                hex!(\"#b8bb26\"),\n                hex!(\"#fabd2f\"),\n                hex!(\"#83a598\"),\n                hex!(\"#d3869b\"),\n                hex!(\"#d65d0e\"),\n                hex!(\"#9d0006\"),\n                hex!(\"#79740e\"),\n                hex!(\"#b57614\"),\n                hex!(\"#076678\"),\n                hex!(\"#8f3f71\"),\n                hex!(\"#427b58\"),\n                hex!(\"#d65d03\"),\n                hex!(\"#af3a03\"),\n            ],\n            border_style: hex!(\"#3c3836\"),\n            highlighted_border_style: hex!(\"#af3a03\"),\n            text_style: hex!(\"#3c3836\"),\n            selected_text_style: hex!(\"#ebdbb2\").bg(hex_colour!(\"#3c3836\")),\n            table_header_style: hex!(\"#076678\").add_modifier(Modifier::BOLD),\n            widget_title_style: hex!(\"#3c3836\"),\n            graph_style: hex!(\"#3c3836\"),\n            graph_legend_style: hex!(\"#3c3836\"),\n            high_battery: hex!(\"#98971a\"),\n            medium_battery: hex!(\"#d79921\"),\n            low_battery: hex!(\"#cc241d\"),\n            invalid_query_style: color!(Color::Red),\n            disabled_text_style: hex!(\"#d5c4a1\"),\n            border_type: BorderType::Plain,\n            #[cfg(target_os = \"linux\")]\n            thread_text_style: hex!(\"#458588\"),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn gruvbox_palettes_valid() {\n        let _ = super::Styles::gruvbox_palette();\n        let _ = super::Styles::gruvbox_light_palette();\n    }\n}\n"
  },
  {
    "path": "src/options/config/style/themes/nord.rs",
    "content": "use tui::{\n    style::{Color, Modifier},\n    widgets::BorderType,\n};\n\nuse super::{color, hex};\nuse crate::options::config::style::{Styles, themes::hex_colour};\n\nimpl Styles {\n    pub(crate) fn nord_palette() -> Self {\n        Self {\n            ram_style: hex!(\"#88c0d0\"),\n            #[cfg(not(target_os = \"windows\"))]\n            cache_style: hex!(\"#d8dee9\"),\n            swap_style: hex!(\"#d08770\"),\n            #[cfg(feature = \"zfs\")]\n            arc_style: hex!(\"#5e81ac\"),\n            #[cfg(feature = \"gpu\")]\n            gpu_colours: vec![\n                hex!(\"#8fbcbb\"),\n                hex!(\"#81a1c1\"),\n                hex!(\"#d8dee9\"),\n                hex!(\"#b48ead\"),\n                hex!(\"#a3be8c\"),\n                hex!(\"#ebcb8b\"),\n                hex!(\"#bf616a\"),\n            ],\n            rx_style: hex!(\"#88c0d0\"),\n            tx_style: hex!(\"#d08770\"),\n            total_rx_style: hex!(\"#5e81ac\"),\n            total_tx_style: hex!(\"#8fbcbb\"),\n            all_cpu_colour: hex!(\"#88c0d0\"),\n            avg_cpu_colour: hex!(\"#8fbcbb\"),\n            cpu_colour_styles: vec![\n                hex!(\"#5e81ac\"),\n                hex!(\"#81a1c1\"),\n                hex!(\"#d8dee9\"),\n                hex!(\"#b48ead\"),\n                hex!(\"#a3be8c\"),\n                hex!(\"#ebcb8b\"),\n                hex!(\"#d08770\"),\n                hex!(\"#bf616a\"),\n            ],\n            border_style: hex!(\"#88c0d0\"),\n            highlighted_border_style: hex!(\"#5e81ac\"),\n            text_style: hex!(\"#e5e9f0\"),\n            selected_text_style: hex!(\"#2e3440\").bg(hex_colour!(\"#88c0d0\")),\n            table_header_style: hex!(\"#81a1c1\").add_modifier(Modifier::BOLD),\n            widget_title_style: hex!(\"#e5e9f0\"),\n            graph_style: hex!(\"#e5e9f0\"),\n            graph_legend_style: hex!(\"#e5e9f0\"),\n            high_battery: hex!(\"#a3be8c\"),\n            medium_battery: hex!(\"#ebcb8b\"),\n            low_battery: hex!(\"#bf616a\"),\n            invalid_query_style: color!(Color::Red),\n            disabled_text_style: hex!(\"#4c566a\"),\n            border_type: BorderType::Plain,\n            #[cfg(target_os = \"linux\")]\n            thread_text_style: hex!(\"#a3be8c\"),\n        }\n    }\n\n    pub(crate) fn nord_light_palette() -> Self {\n        Self {\n            ram_style: hex!(\"#81a1c1\"),\n            #[cfg(not(target_os = \"windows\"))]\n            cache_style: hex!(\"#4c566a\"),\n            swap_style: hex!(\"#d08770\"),\n            #[cfg(feature = \"zfs\")]\n            arc_style: hex!(\"#5e81ac\"),\n            #[cfg(feature = \"gpu\")]\n            gpu_colours: vec![\n                hex!(\"#8fbcbb\"),\n                hex!(\"#88c0d0\"),\n                hex!(\"#4c566a\"),\n                hex!(\"#b48ead\"),\n                hex!(\"#a3be8c\"),\n                hex!(\"#ebcb8b\"),\n                hex!(\"#bf616a\"),\n            ],\n            rx_style: hex!(\"#81a1c1\"),\n            tx_style: hex!(\"#d08770\"),\n            total_rx_style: hex!(\"#5e81ac\"),\n            total_tx_style: hex!(\"#8fbcbb\"),\n            all_cpu_colour: hex!(\"#81a1c1\"),\n            avg_cpu_colour: hex!(\"#8fbcbb\"),\n            cpu_colour_styles: vec![\n                hex!(\"#5e81ac\"),\n                hex!(\"#88c0d0\"),\n                hex!(\"#4c566a\"),\n                hex!(\"#b48ead\"),\n                hex!(\"#a3be8c\"),\n                hex!(\"#ebcb8b\"),\n                hex!(\"#d08770\"),\n                hex!(\"#bf616a\"),\n            ],\n            border_style: hex!(\"#2e3440\"),\n            highlighted_border_style: hex!(\"#5e81ac\"),\n            text_style: hex!(\"#2e3440\"),\n            selected_text_style: hex!(\"#f5f5f5\").bg(hex_colour!(\"#5e81ac\")),\n            table_header_style: hex!(\"#5e81ac\").add_modifier(Modifier::BOLD),\n            widget_title_style: hex!(\"#2e3440\"),\n            graph_style: hex!(\"#2e3440\"),\n            graph_legend_style: hex!(\"#2e3440\"),\n            high_battery: hex!(\"#a3be8c\"),\n            medium_battery: hex!(\"#ebcb8b\"),\n            low_battery: hex!(\"#bf616a\"),\n            invalid_query_style: color!(Color::Red),\n            disabled_text_style: hex!(\"#d8dee9\"),\n            border_type: BorderType::Plain,\n            #[cfg(target_os = \"linux\")]\n            thread_text_style: hex!(\"#a3be8c\"),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn nord_palettes_valid() {\n        let _ = super::Styles::nord_palette();\n        let _ = super::Styles::nord_light_palette();\n    }\n}\n"
  },
  {
    "path": "src/options/config/style/themes.rs",
    "content": "//! A set of pre-defined themes.\n\npub(super) mod default;\npub(super) mod gruvbox;\npub(super) mod nord;\n\n/// Convert a [`tui::style::Color`] into a [`tui::style::Style`] with the color as the foreground.\nmacro_rules! color {\n    ($value:expr) => {\n        tui::style::Style::new().fg($value)\n    };\n}\n\n/// Convert a hex string to a [`tui::style::Style`], where the hex string is used as the foreground color.\nmacro_rules! hex {\n    ($value:literal) => {\n        tui::style::Style::new().fg(crate::options::config::style::utils::try_hex_to_colour(\n            $value.into(),\n        )\n        .expect(\"valid hex\"))\n    };\n}\n\n/// Convert a hex string to a [`tui::style::Color`].\nmacro_rules! hex_colour {\n    ($value:literal) => {\n        crate::options::config::style::utils::try_hex_to_colour($value.into()).expect(\"valid hex\")\n    };\n}\n\npub(super) use color;\npub(super) use hex;\npub(super) use hex_colour;\n"
  },
  {
    "path": "src/options/config/style/utils.rs",
    "content": "use concat_string::concat_string;\nuse tui::style::Color;\nuse unicode_segmentation::UnicodeSegmentation;\n\n/// Convert a hex string to a colour.\npub(super) fn try_hex_to_colour(hex: &str) -> Result<Color, String> {\n    fn hex_component_to_int(hex: &str, first: &str, second: &str) -> Result<u8, String> {\n        u8::from_str_radix(&concat_string!(first, second), 16)\n            .map_err(|_| format!(\"'{hex}' is an invalid hex color, could not decode.\"))\n    }\n\n    fn invalid_hex_format(hex: &str) -> String {\n        format!(\n            \"'{hex}' is an invalid hex color. It must be either a 7 character hex string of the form '#12ab3c' or a 3 character hex string of the form '#1a2'.\",\n        )\n    }\n\n    if !hex.starts_with('#') {\n        return Err(invalid_hex_format(hex));\n    }\n\n    let components: Vec<&str> = hex.graphemes(true).collect();\n    if components.len() == 7 {\n        // A 6-long hex.\n        let r = hex_component_to_int(hex, components[1], components[2])?;\n        let g = hex_component_to_int(hex, components[3], components[4])?;\n        let b = hex_component_to_int(hex, components[5], components[6])?;\n\n        Ok(Color::Rgb(r, g, b))\n    } else if components.len() == 4 {\n        // A 3-long hex.\n        let r = hex_component_to_int(hex, components[1], components[1])?;\n        let g = hex_component_to_int(hex, components[2], components[2])?;\n        let b = hex_component_to_int(hex, components[3], components[3])?;\n\n        Ok(Color::Rgb(r, g, b))\n    } else {\n        Err(invalid_hex_format(hex))\n    }\n}\n\npub fn str_to_colour(input_val: &str) -> Result<Color, String> {\n    if input_val.len() > 1 {\n        if input_val.starts_with('#') {\n            try_hex_to_colour(input_val)\n        } else if input_val.contains(',') {\n            convert_rgb_to_color(input_val)\n        } else {\n            convert_name_to_colour(input_val)\n        }\n    } else {\n        Err(format!(\"Value '{input_val}' is not valid.\",))\n    }\n}\n\nfn convert_rgb_to_color(rgb_str: &str) -> Result<Color, String> {\n    let rgb_list = rgb_str.split(',').collect::<Vec<&str>>();\n    if rgb_list.len() != 3 {\n        return Err(format!(\n            \"Value '{rgb_str}' is an invalid RGB colour. It must be a comma separated value with 3 integers from 0 to 255 (ie: '255, 0, 155').\",\n        ));\n    }\n\n    let rgb = rgb_list\n        .iter()\n        .filter_map(|val| (*(*val)).to_string().trim().parse::<u8>().ok())\n        .collect::<Vec<_>>();\n\n    if rgb.len() == 3 {\n        Ok(Color::Rgb(rgb[0], rgb[1], rgb[2]))\n    } else {\n        Err(format!(\n            \"Value '{rgb_str}' contained invalid RGB values. It must be a comma separated value with 3 integers from 0 to 255 (ie: '255, 0, 155').\",\n        ))\n    }\n}\n\nfn convert_name_to_colour(color_name: &str) -> Result<Color, String> {\n    match color_name.to_lowercase().trim() {\n        \"reset\" => Ok(Color::Reset),\n        \"black\" => Ok(Color::Black),\n        \"red\" => Ok(Color::Red),\n        \"green\" => Ok(Color::Green),\n        \"yellow\" => Ok(Color::Yellow),\n        \"blue\" => Ok(Color::Blue),\n        \"magenta\" => Ok(Color::Magenta),\n        \"cyan\" => Ok(Color::Cyan),\n        \"gray\" | \"grey\" => Ok(Color::Gray),\n        \"darkgray\" | \"darkgrey\" | \"dark gray\" | \"dark grey\" => Ok(Color::DarkGray),\n        \"lightred\" | \"light red\" => Ok(Color::LightRed),\n        \"lightgreen\" | \"light green\" => Ok(Color::LightGreen),\n        \"lightyellow\" | \"light yellow\" => Ok(Color::LightYellow),\n        \"lightblue\" | \"light blue\" => Ok(Color::LightBlue),\n        \"lightmagenta\" | \"light magenta\" => Ok(Color::LightMagenta),\n        \"lightcyan\" | \"light cyan\" => Ok(Color::LightCyan),\n        \"white\" => Ok(Color::White),\n        _ => Err(format!(\n            \"'{color_name}' is an invalid named color.\n            \nThe following are supported named colors: \n+--------+-------------+---------------------+\n|  Reset | Magenta     | Light Yellow        |\n+--------+-------------+---------------------+\n|  Black | Cyan        | Light Blue          |\n+--------+-------------+---------------------+\n|   Red  | Gray/Grey   | Light Magenta       |\n+--------+-------------+---------------------+\n|  Green | Light Cyan  | Dark Gray/Dark Grey |\n+--------+-------------+---------------------+\n| Yellow | Light Red   | White               |\n+--------+-------------+---------------------+\n|  Blue  | Light Green |                     |\n+--------+-------------+---------------------+\n\nAlternatively, hex colors or RGB color codes are valid.\\n\"\n        )),\n    }\n}\n\nmacro_rules! opt {\n    ($($e: tt)+) => {\n        (|| { $($e)+ })()\n    }\n}\n\nmacro_rules! set_style {\n    ($palette_field:expr, $config_location:expr, $field:tt) => {\n        if let Some(style) = &(opt!($config_location.as_ref()?.$field.as_ref())) {\n            match &style {\n                TextStyleConfig::Colour(colour) => {\n                    $palette_field = $palette_field.fg(\n                        crate::options::config::style::utils::str_to_colour(&colour.0).map_err(\n                            |err| match stringify!($config_location).split_once(\".\") {\n                                Some((_, loc)) => crate::options::OptionError::config(format!(\n                                    \"Please update 'styles.{loc}.{}' in your config file. {err}\",\n                                    stringify!($field)\n                                )),\n                                None => crate::options::OptionError::config(format!(\n                                    \"Please update 'styles.{}' in your config file. {err}\",\n                                    stringify!($field)\n                                )),\n                            },\n                        )?,\n                    );\n                }\n                TextStyleConfig::TextStyle {\n                    color,\n                    bg_color,\n                    bold,\n                    italics,\n                } => {\n                    if let Some(fg) = &color {\n                        $palette_field = $palette_field\n                            .fg(crate::options::config::style::utils::str_to_colour(\n                            &fg.0,\n                        )\n                        .map_err(|err| {\n                            match stringify!($config_location).split_once(\".\") {\n                                Some((_, loc)) => crate::options::OptionError::config(format!(\n                                    \"Please update 'styles.{loc}.{}' in your config file. {err}\",\n                                    stringify!($field)\n                                )),\n                                None => crate::options::OptionError::config(format!(\n                                    \"Please update 'styles.{}' in your config file. {err}\",\n                                    stringify!($field)\n                                )),\n                            }\n                        })?);\n                    }\n\n                    if let Some(bg) = &bg_color {\n                        $palette_field = $palette_field\n                            .bg(crate::options::config::style::utils::str_to_colour(\n                            &bg.0,\n                        )\n                        .map_err(|err| {\n                            match stringify!($config_location).split_once(\".\") {\n                                Some((_, loc)) => crate::options::OptionError::config(format!(\n                                    \"Please update 'styles.{loc}.{}' in your config file. {err}\",\n                                    stringify!($field)\n                                )),\n                                None => crate::options::OptionError::config(format!(\n                                    \"Please update 'styles.{}' in your config file. {err}\",\n                                    stringify!($field)\n                                )),\n                            }\n                        })?);\n                    }\n\n                    if let Some(bold) = &bold {\n                        if *bold {\n                            $palette_field =\n                                $palette_field.add_modifier(tui::style::Modifier::BOLD);\n                        } else {\n                            $palette_field =\n                                $palette_field.remove_modifier(tui::style::Modifier::BOLD);\n                        }\n                    }\n\n                    if let Some(italics) = &italics {\n                        if *italics {\n                            $palette_field =\n                                $palette_field.add_modifier(tui::style::Modifier::ITALIC);\n                        } else {\n                            $palette_field =\n                                $palette_field.remove_modifier(tui::style::Modifier::ITALIC);\n                        }\n                    }\n                }\n            }\n        }\n    };\n}\n\nmacro_rules! set_colour {\n    ($palette_field:expr, $config_location:expr, $field:tt) => {\n        if let Some(colour) = &(opt!($config_location.as_ref()?.$field.as_ref())) {\n            $palette_field = $palette_field.fg(\n                crate::options::config::style::utils::str_to_colour(&colour.0).map_err(|err| {\n                    match stringify!($config_location).split_once(\".\") {\n                        Some((_, loc)) => crate::options::OptionError::config(format!(\n                            \"Please update 'styles.{loc}.{}' in your config file. {err}\",\n                            stringify!($field)\n                        )),\n                        None => crate::options::OptionError::config(format!(\n                            \"Please update 'styles.{}' in your config file. {err}\",\n                            stringify!($field)\n                        )),\n                    }\n                })?,\n            );\n        }\n    };\n}\n\n/// Set `palette_field` to the value in `config_location` for `field`.\nmacro_rules! set_colour_list {\n    ($palette_field:expr, $config_location:expr, $field:tt) => {\n        if let Some(colour_list) = &(opt!($config_location.as_ref()?.$field.as_ref())) {\n            $palette_field = colour_list\n                .iter()\n                .map(|s| {\n                    Ok(Style::default()\n                        .fg(crate::options::config::style::utils::str_to_colour(&s.0)?))\n                })\n                .collect::<Result<Vec<Style>, String>>()\n                .map_err(|err| match stringify!($config_location).split_once(\".\") {\n                    Some((_, loc)) => crate::options::OptionError::config(format!(\n                        \"Please update 'styles.{loc}.{}' in your config file. {err}\",\n                        stringify!($field)\n                    )),\n                    None => crate::options::OptionError::config(format!(\n                        \"Please update 'styles.{}' in your config file. {err}\",\n                        stringify!($field)\n                    )),\n                })?;\n        }\n    };\n}\n\npub(super) use opt;\npub(super) use set_colour;\npub(super) use set_colour_list;\npub(super) use set_style;\n\n#[cfg(test)]\nmod test {\n\n    use tui::style::{Modifier, Style};\n\n    use super::*;\n    use crate::options::config::style::{ColorStr, TextStyleConfig};\n\n    #[test]\n    fn general_str_to_colour() {\n        assert_eq!(str_to_colour(\"red\").unwrap(), Color::Red);\n        assert!(str_to_colour(\"r ed\").is_err());\n\n        assert_eq!(str_to_colour(\"#ffffff\").unwrap(), Color::Rgb(255, 255, 255));\n        assert!(str_to_colour(\"#fff fff\").is_err());\n\n        assert_eq!(\n            str_to_colour(\"255, 255, 255\").unwrap(),\n            Color::Rgb(255, 255, 255)\n        );\n        assert!(str_to_colour(\"255, 256, 255\").is_err());\n    }\n\n    #[test]\n    fn invalid_colour_names() {\n        // Test invalid spacing in single word.\n        assert!(convert_name_to_colour(\"bl ack\").is_err());\n\n        // Test invalid spacing in dual word.\n        assert!(convert_name_to_colour(\"darkg ray\").is_err());\n\n        // Test completely invalid colour.\n        assert!(convert_name_to_colour(\"darkreset\").is_err());\n    }\n\n    #[test]\n    fn valid_colour_names() {\n        // Standard color should work\n        assert_eq!(convert_name_to_colour(\"red\"), Ok(Color::Red));\n\n        // Capitalizing should be fine.\n        assert_eq!(convert_name_to_colour(\"RED\"), Ok(Color::Red));\n\n        // Spacing shouldn't be an issue now.\n        assert_eq!(convert_name_to_colour(\" red \"), Ok(Color::Red));\n\n        // The following are all equivalent.\n        assert_eq!(convert_name_to_colour(\"darkgray\"), Ok(Color::DarkGray));\n        assert_eq!(convert_name_to_colour(\"darkgrey\"), Ok(Color::DarkGray));\n        assert_eq!(convert_name_to_colour(\"dark grey\"), Ok(Color::DarkGray));\n        assert_eq!(convert_name_to_colour(\"dark gray\"), Ok(Color::DarkGray));\n\n        assert_eq!(convert_name_to_colour(\"grey\"), Ok(Color::Gray));\n        assert_eq!(convert_name_to_colour(\"gray\"), Ok(Color::Gray));\n\n        // One more test with spacing.\n        assert_eq!(\n            convert_name_to_colour(\" lightmagenta \"),\n            Ok(Color::LightMagenta)\n        );\n        assert_eq!(\n            convert_name_to_colour(\"light magenta\"),\n            Ok(Color::LightMagenta)\n        );\n        assert_eq!(\n            convert_name_to_colour(\" light magenta \"),\n            Ok(Color::LightMagenta)\n        );\n    }\n\n    #[test]\n    fn valid_hex_colours() {\n        // Check hex with 6 characters.\n        assert_eq!(\n            try_hex_to_colour(\"#ffffff\").unwrap(),\n            Color::Rgb(255, 255, 255)\n        );\n        assert_eq!(try_hex_to_colour(\"#000000\").unwrap(), Color::Rgb(0, 0, 0));\n        try_hex_to_colour(\"#111111\").unwrap();\n        try_hex_to_colour(\"#11ff11\").unwrap();\n        try_hex_to_colour(\"#1f1f1f\").unwrap();\n        assert_eq!(\n            try_hex_to_colour(\"#123abc\").unwrap(),\n            Color::Rgb(18, 58, 188)\n        );\n\n        // Check hex with 3 characters.\n        assert_eq!(\n            try_hex_to_colour(\"#fff\").unwrap(),\n            Color::Rgb(255, 255, 255)\n        );\n        assert_eq!(try_hex_to_colour(\"#000\").unwrap(), Color::Rgb(0, 0, 0));\n        try_hex_to_colour(\"#111\").unwrap();\n        try_hex_to_colour(\"#1f1\").unwrap();\n        try_hex_to_colour(\"#f1f\").unwrap();\n        try_hex_to_colour(\"#ff1\").unwrap();\n        try_hex_to_colour(\"#1ab\").unwrap();\n        assert_eq!(try_hex_to_colour(\"#1ab\").unwrap(), Color::Rgb(17, 170, 187));\n    }\n\n    #[test]\n    fn invalid_hex_colours() {\n        assert!(try_hex_to_colour(\"ffffff\").is_err());\n        assert!(try_hex_to_colour(\"111111\").is_err());\n\n        assert!(try_hex_to_colour(\"fff\").is_err());\n        assert!(try_hex_to_colour(\"111\").is_err());\n        assert!(try_hex_to_colour(\"fffffff\").is_err());\n        assert!(try_hex_to_colour(\"1234567\").is_err());\n\n        assert!(try_hex_to_colour(\"#fffffff\").is_err());\n        assert!(try_hex_to_colour(\"#1234567\").is_err());\n        assert!(try_hex_to_colour(\"#ff\").is_err());\n        assert!(try_hex_to_colour(\"#12\").is_err());\n        assert!(try_hex_to_colour(\"\").is_err());\n\n        assert!(try_hex_to_colour(\"#pppppp\").is_err());\n        assert!(try_hex_to_colour(\"#00000p\").is_err());\n        assert!(try_hex_to_colour(\"#ppp\").is_err());\n\n        assert!(try_hex_to_colour(\"#一\").is_err());\n        assert!(try_hex_to_colour(\"#一二\").is_err());\n        assert!(try_hex_to_colour(\"#一二三\").is_err());\n        assert!(try_hex_to_colour(\"#一二三四\").is_err());\n\n        assert!(try_hex_to_colour(\"#f一f\").is_err());\n        assert!(try_hex_to_colour(\"#ff一11\").is_err());\n\n        assert!(try_hex_to_colour(\"#🇨🇦\").is_err());\n        assert!(try_hex_to_colour(\"#🇨🇦🇨🇦\").is_err());\n        assert!(try_hex_to_colour(\"#🇨🇦🇨🇦🇨🇦\").is_err());\n        assert!(try_hex_to_colour(\"#🇨🇦🇨🇦🇨🇦🇨🇦\").is_err());\n\n        assert!(try_hex_to_colour(\"#हिन्दी\").is_err());\n    }\n\n    #[test]\n    fn test_rgb_colours() {\n        assert_eq!(\n            convert_rgb_to_color(\"0, 0, 0\").unwrap(),\n            Color::Rgb(0, 0, 0)\n        );\n        assert_eq!(\n            convert_rgb_to_color(\"255, 255, 255\").unwrap(),\n            Color::Rgb(255, 255, 255)\n        );\n        assert!(convert_rgb_to_color(\"255, 256, 255\").is_err());\n        assert!(convert_rgb_to_color(\"256, 0, 256\").is_err());\n        assert!(convert_rgb_to_color(\"1, -1, 1\").is_err());\n        assert!(convert_rgb_to_color(\"1, -100000, 1\").is_err());\n        assert!(convert_rgb_to_color(\"1, -100000, 100000\").is_err());\n    }\n\n    struct DummyConfig {\n        inner: Option<InnerDummyConfig>,\n    }\n\n    struct InnerDummyConfig {\n        color_a: Option<ColorStr>,\n        color_b: Option<ColorStr>,\n        color_c: Option<ColorStr>,\n        color_d: Option<ColorStr>,\n        many_colors: Option<Vec<ColorStr>>,\n        text_a: Option<TextStyleConfig>,\n        text_b: Option<TextStyleConfig>,\n        text_c: Option<TextStyleConfig>,\n        text_d: Option<TextStyleConfig>,\n        text_e: Option<TextStyleConfig>,\n        bad_color: Option<ColorStr>,\n        bad_list: Option<Vec<ColorStr>>,\n        bad_text_a: Option<TextStyleConfig>,\n        bad_text_b: Option<TextStyleConfig>,\n    }\n\n    impl Default for InnerDummyConfig {\n        fn default() -> Self {\n            Self {\n                color_a: None,\n                color_b: Some(ColorStr(\"red\".into())),\n                color_c: Some(ColorStr(\"255, 255, 255\".into())),\n                color_d: Some(ColorStr(\"#000000\".into())),\n                many_colors: Some(vec![ColorStr(\"red\".into()), ColorStr(\"blue\".into())]),\n                text_a: Some(TextStyleConfig::Colour(ColorStr(\"green\".into()))),\n                text_b: Some(TextStyleConfig::TextStyle {\n                    color: None,\n                    bg_color: None,\n                    bold: None,\n                    italics: None,\n                }),\n                text_c: Some(TextStyleConfig::TextStyle {\n                    color: Some(ColorStr(\"magenta\".into())),\n                    bg_color: Some(ColorStr(\"255, 255, 255\".into())),\n                    bold: Some(true),\n                    italics: Some(false),\n                }),\n                text_d: Some(TextStyleConfig::TextStyle {\n                    color: Some(ColorStr(\"#fff\".into())),\n                    bg_color: Some(ColorStr(\"1, 1, 1\".into())),\n                    bold: Some(false),\n                    italics: Some(true),\n                }),\n                text_e: None,\n                bad_color: Some(ColorStr(\"asdf\".into())),\n                bad_list: Some(vec![\n                    ColorStr(\"red\".into()),\n                    ColorStr(\"asdf\".into()),\n                    ColorStr(\"ghi\".into()),\n                ]),\n                bad_text_a: Some(TextStyleConfig::TextStyle {\n                    color: Some(ColorStr(\"asdf\".into())),\n                    bg_color: None,\n                    bold: None,\n                    italics: None,\n                }),\n                bad_text_b: Some(TextStyleConfig::TextStyle {\n                    color: None,\n                    bg_color: Some(ColorStr(\"asdf\".into())),\n                    bold: None,\n                    italics: None,\n                }),\n            }\n        }\n    }\n\n    #[test]\n    fn test_set_colour() -> anyhow::Result<()> {\n        let mut s = Style::default().fg(Color::Black);\n        let dummy = DummyConfig {\n            inner: Some(InnerDummyConfig::default()),\n        };\n\n        set_colour!(s, &dummy.inner, color_a);\n        assert_eq!(s.fg.unwrap(), Color::Black);\n        assert_eq!(s.bg, None);\n\n        set_colour!(s, &dummy.inner, color_b);\n        assert_eq!(s.fg.unwrap(), Color::Red);\n        assert_eq!(s.bg, None);\n\n        set_colour!(s, &dummy.inner, color_c);\n        assert_eq!(s.fg.unwrap(), Color::Rgb(255, 255, 255));\n        assert_eq!(s.bg, None);\n\n        set_colour!(s, &dummy.inner, color_d);\n        assert_eq!(s.fg.unwrap(), Color::Rgb(0, 0, 0));\n        assert_eq!(s.bg, None);\n\n        Ok(())\n    }\n\n    #[test]\n    fn test_bad_set_colour() {\n        let mut _s = Style::default().fg(Color::Black);\n        let dummy = DummyConfig {\n            inner: Some(InnerDummyConfig::default()),\n        };\n\n        (move || -> anyhow::Result<()> {\n            set_colour!(_s, &dummy.inner, bad_color);\n\n            Ok(())\n        })()\n        .unwrap_err();\n    }\n\n    #[test]\n    fn test_set_multi_colours() -> anyhow::Result<()> {\n        let mut s: Vec<Style> = vec![];\n        let dummy = DummyConfig {\n            inner: Some(InnerDummyConfig::default()),\n        };\n\n        set_colour_list!(s, &dummy.inner, many_colors);\n        assert_eq!(s.len(), 2);\n        assert_eq!(s[0].fg, Some(Color::Red));\n        assert_eq!(s[1].fg, Some(Color::Blue));\n\n        Ok(())\n    }\n\n    #[test]\n    fn test_bad_set_list() {\n        let dummy = DummyConfig {\n            inner: Some(InnerDummyConfig::default()),\n        };\n\n        (move || -> anyhow::Result<()> {\n            let mut _s: Vec<Style>;\n            set_colour_list!(_s, &dummy.inner, bad_list);\n\n            Ok(())\n        })()\n        .unwrap_err();\n    }\n\n    #[test]\n    fn test_set_style() -> anyhow::Result<()> {\n        let mut s = Style::default().fg(Color::Black);\n        let dummy = DummyConfig {\n            inner: Some(InnerDummyConfig::default()),\n        };\n\n        set_style!(s, &dummy.inner, text_e);\n        assert_eq!(s.fg.unwrap(), Color::Black);\n        assert_eq!(s.bg, None);\n        assert!(s.add_modifier.is_empty());\n\n        set_style!(s, &dummy.inner, text_a);\n        assert_eq!(s.fg.unwrap(), Color::Green);\n        assert_eq!(s.bg, None);\n\n        set_style!(s, &dummy.inner, text_b);\n        assert_eq!(s.fg.unwrap(), Color::Green);\n        assert_eq!(s.bg, None);\n\n        set_style!(s, &dummy.inner, text_c);\n        assert_eq!(s.fg.unwrap(), Color::Magenta);\n        assert_eq!(s.bg.unwrap(), Color::Rgb(255, 255, 255));\n        assert!(s.add_modifier.contains(Modifier::BOLD));\n        assert!(!s.add_modifier.contains(Modifier::ITALIC));\n\n        set_style!(s, &dummy.inner, text_d);\n        assert_eq!(s.fg.unwrap(), Color::Rgb(255, 255, 255));\n        assert_eq!(s.bg.unwrap(), Color::Rgb(1, 1, 1));\n        assert!(!s.add_modifier.contains(Modifier::BOLD));\n        assert!(s.add_modifier.contains(Modifier::ITALIC));\n\n        Ok(())\n    }\n\n    #[test]\n    fn test_bad_text_1() {\n        let mut _s = Style::default().fg(Color::Black);\n        let dummy = DummyConfig {\n            inner: Some(InnerDummyConfig::default()),\n        };\n\n        (move || -> anyhow::Result<()> {\n            set_style!(_s, &dummy.inner, bad_text_a);\n\n            Ok(())\n        })()\n        .unwrap_err();\n    }\n\n    #[test]\n    fn test_bad_text_2() {\n        let mut _s = Style::default().fg(Color::Black);\n        let dummy = DummyConfig {\n            inner: Some(InnerDummyConfig::default()),\n        };\n        (move || -> anyhow::Result<()> {\n            set_style!(_s, &dummy.inner, bad_text_b);\n\n            Ok(())\n        })()\n        .unwrap_err();\n    }\n}\n"
  },
  {
    "path": "src/options/config/style/widgets.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse super::{ColorStr, TextStyleConfig, borders::WidgetBorderType};\n\n/// General styling for generic widgets.\n#[derive(Clone, Debug, Default, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct WidgetStyle {\n    /// The colour of the widgets' borders.\n    #[serde(alias = \"border_colour\")]\n    pub(crate) border_color: Option<ColorStr>,\n\n    /// The colour of a widget's borders when the widget is selected.\n    #[serde(alias = \"selected_border_colour\")]\n    pub(crate) selected_border_color: Option<ColorStr>,\n\n    /// Text styling for a widget's title.\n    pub(crate) widget_title: Option<TextStyleConfig>,\n\n    /// Text styling for text in general.\n    pub(crate) text: Option<TextStyleConfig>,\n\n    /// Text styling for text when representing something that is selected.\n    pub(crate) selected_text: Option<TextStyleConfig>,\n\n    /// Text styling for text when representing something that is disabled.\n    pub(crate) disabled_text: Option<TextStyleConfig>,\n\n    /// Text styling for text when representing process threads. Only usable\n    /// on Linux at the moment.\n    pub(crate) thread_text: Option<TextStyleConfig>,\n\n    /// Widget borders type.\n    pub(crate) widget_border_type: Option<WidgetBorderType>,\n}\n"
  },
  {
    "path": "src/options/config/style.rs",
    "content": "//! Config options around styling.\n\nmod battery;\nmod borders;\nmod cpu;\nmod graphs;\nmod memory;\nmod network;\nmod tables;\nmod themes;\nmod utils;\nmod widgets;\n\nuse std::borrow::Cow;\n\nuse battery::BatteryStyle;\nuse cpu::CpuStyle;\nuse graphs::GraphStyle;\nuse memory::MemoryStyle;\nuse network::NetworkStyle;\nuse serde::{Deserialize, Serialize};\nuse tables::TableStyle;\nuse tui::{style::Style, widgets::BorderType};\nuse utils::{opt, set_colour, set_colour_list, set_style};\nuse widgets::WidgetStyle;\n\nuse super::Config;\nuse crate::options::{OptionError, OptionResult, args::BottomArgs};\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, derive(PartialEq, Eq))]\npub(crate) struct ColorStr(Cow<'static, str>);\n\n/// A style for text.\n#[derive(Clone, Debug, Deserialize, Serialize)]\n#[serde(untagged)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) enum TextStyleConfig {\n    Colour(ColorStr),\n    TextStyle {\n        /// A built-in ANSI colour, RGB hex, or RGB colour code.\n        #[serde(alias = \"colour\")]\n        color: Option<ColorStr>,\n\n        /// A built-in ANSI colour, RGB hex, or RGB colour code.\n        #[serde(alias = \"bg_colour\")]\n        bg_color: Option<ColorStr>,\n\n        /// Whether to make this text bolded or not. If not set,\n        /// will default to built-in defaults.\n        bold: Option<bool>,\n\n        /// Whether to make this text italicized or not. If not set,\n        /// will default to built-in defaults.\n        italics: Option<bool>,\n    },\n}\n\n/// Style-related configs.\n#[derive(Clone, Debug, Default, Deserialize, Serialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct StyleConfig {\n    /// A built-in theme.\n    ///\n    /// If this is and a custom colour are both set, in the config file,\n    /// the custom colour scheme will be prioritized first. If a theme\n    /// is set in the command-line args, however, it will always be\n    /// prioritized first.\n    pub(crate) theme: Option<Cow<'static, str>>,\n\n    /// Styling for the CPU widget.\n    pub(crate) cpu: Option<CpuStyle>,\n\n    /// Styling for the memory widget.\n    pub(crate) memory: Option<MemoryStyle>,\n\n    /// Styling for the network widget.\n    pub(crate) network: Option<NetworkStyle>,\n\n    /// Styling for the battery widget.\n    pub(crate) battery: Option<BatteryStyle>,\n\n    /// Styling for table widgets.\n    pub(crate) tables: Option<TableStyle>,\n\n    /// Styling for graph widgets.\n    pub(crate) graphs: Option<GraphStyle>,\n\n    /// Styling for general widgets.\n    pub(crate) widgets: Option<WidgetStyle>,\n}\n\n/// The actual internal representation of the configured styles.\n#[derive(Debug)]\npub struct Styles {\n    pub(crate) ram_style: Style,\n    #[cfg(not(target_os = \"windows\"))]\n    pub(crate) cache_style: Style,\n    pub(crate) swap_style: Style,\n    #[cfg(feature = \"zfs\")]\n    pub(crate) arc_style: Style,\n    #[cfg(feature = \"gpu\")]\n    pub(crate) gpu_colours: Vec<Style>,\n    pub(crate) rx_style: Style,\n    pub(crate) tx_style: Style,\n    pub(crate) total_rx_style: Style,\n    pub(crate) total_tx_style: Style,\n    pub(crate) all_cpu_colour: Style,\n    pub(crate) avg_cpu_colour: Style,\n    pub(crate) cpu_colour_styles: Vec<Style>,\n    pub(crate) border_style: Style,\n    pub(crate) highlighted_border_style: Style,\n    pub(crate) text_style: Style,\n    pub(crate) selected_text_style: Style,\n    pub(crate) table_header_style: Style,\n    pub(crate) widget_title_style: Style,\n    pub(crate) graph_style: Style,\n    pub(crate) graph_legend_style: Style,\n    pub(crate) high_battery: Style,\n    pub(crate) medium_battery: Style,\n    pub(crate) low_battery: Style,\n    pub(crate) invalid_query_style: Style,\n    pub(crate) disabled_text_style: Style,\n    #[cfg(target_os = \"linux\")]\n    pub(crate) thread_text_style: Style,\n    pub(crate) border_type: BorderType,\n}\n\nimpl Default for Styles {\n    fn default() -> Self {\n        Self::default_palette()\n    }\n}\n\nimpl Styles {\n    pub fn new(args: &BottomArgs, config: &Config) -> anyhow::Result<Self> {\n        let mut palette = match &args.style.theme {\n            Some(theme) => Self::from_theme(theme)?,\n            None => match config.styles.as_ref().and_then(|s| s.theme.as_ref()) {\n                Some(theme) => Self::from_theme(theme)?,\n                None => Self::default(),\n            },\n        };\n\n        // Apply theme from config on top.\n        if let Some(config_style) = &config.styles {\n            palette.set_styles_from_config(config_style)?;\n        }\n\n        Ok(palette)\n    }\n\n    fn from_theme(theme: &str) -> anyhow::Result<Self> {\n        let lower_case = theme.to_lowercase();\n        match lower_case.as_str() {\n            \"default\" => Ok(Self::default_palette()),\n            \"default-light\" => Ok(Self::default_light_palette()),\n            \"gruvbox\" => Ok(Self::gruvbox_palette()),\n            \"gruvbox-light\" => Ok(Self::gruvbox_light_palette()),\n            \"nord\" => Ok(Self::nord_palette()),\n            \"nord-light\" => Ok(Self::nord_light_palette()),\n            _ => Err(\n                OptionError::other(format!(\"'{theme}' is an invalid built-in color scheme.\"))\n                    .into(),\n            ),\n        }\n    }\n\n    fn set_styles_from_config(&mut self, config: &StyleConfig) -> OptionResult<()> {\n        // CPU\n        set_colour!(self.avg_cpu_colour, config.cpu, avg_entry_color);\n        set_colour!(self.all_cpu_colour, config.cpu, all_entry_color);\n        set_colour_list!(self.cpu_colour_styles, config.cpu, cpu_core_colors);\n\n        // Memory\n        set_colour!(self.ram_style, config.memory, ram_color);\n        set_colour!(self.swap_style, config.memory, swap_color);\n\n        #[cfg(not(target_os = \"windows\"))]\n        set_colour!(self.cache_style, config.memory, cache_color);\n\n        #[cfg(feature = \"zfs\")]\n        set_colour!(self.arc_style, config.memory, arc_color);\n\n        #[cfg(feature = \"gpu\")]\n        set_colour_list!(self.gpu_colours, config.memory, gpu_colors);\n\n        // Network\n        set_colour!(self.rx_style, config.network, rx_color);\n        set_colour!(self.tx_style, config.network, tx_color);\n        set_colour!(self.total_rx_style, config.network, rx_total_color);\n        set_colour!(self.total_tx_style, config.network, tx_total_color);\n\n        // Battery\n        set_colour!(self.high_battery, config.battery, high_battery_color);\n        set_colour!(self.medium_battery, config.battery, medium_battery_color);\n        set_colour!(self.low_battery, config.battery, low_battery_color);\n\n        // Tables\n        set_style!(self.table_header_style, config.tables, headers);\n\n        // Widget graphs\n        set_colour!(self.graph_style, config.graphs, graph_color);\n        set_style!(self.graph_legend_style, config.graphs, legend_text);\n\n        // General widget text.\n        set_style!(self.widget_title_style, config.widgets, widget_title);\n        set_style!(self.text_style, config.widgets, text);\n        set_style!(self.selected_text_style, config.widgets, selected_text);\n        set_style!(self.disabled_text_style, config.widgets, disabled_text);\n\n        #[cfg(target_os = \"linux\")]\n        {\n            set_style!(self.thread_text_style, config.widgets, thread_text);\n        }\n\n        // Widget borders\n        set_colour!(self.border_style, config.widgets, border_color);\n        set_colour!(\n            self.highlighted_border_style,\n            config.widgets,\n            selected_border_color\n        );\n\n        if let Some(widgets) = &config.widgets {\n            if let Some(widget_borders) = widgets.widget_border_type {\n                self.border_type = widget_borders.into();\n            }\n        }\n\n        Ok(())\n    }\n}\n\n#[cfg(test)]\nmod test {\n\n    use tui::style::{Color, Style};\n\n    use super::Styles;\n    use crate::options::config::style::utils::str_to_colour;\n\n    #[test]\n    fn default_selected_colour_works() {\n        let mut colours = Styles::default();\n        let original_selected_text_colour =\n            Styles::default_palette().selected_text_style.fg.unwrap();\n        let original_selected_bg_colour = Styles::default_palette().selected_text_style.bg.unwrap();\n\n        assert_eq!(\n            colours.selected_text_style,\n            Style::default()\n                .fg(original_selected_text_colour)\n                .bg(original_selected_bg_colour),\n        );\n\n        colours.selected_text_style = colours\n            .selected_text_style\n            .fg(str_to_colour(\"magenta\").unwrap())\n            .bg(str_to_colour(\"red\").unwrap());\n\n        assert_eq!(\n            colours.selected_text_style,\n            Style::default().fg(Color::Magenta).bg(Color::Red),\n        );\n    }\n\n    #[test]\n    fn built_in_colour_schemes_work() {\n        Styles::from_theme(\"default\").unwrap();\n        Styles::from_theme(\"default-light\").unwrap();\n        Styles::from_theme(\"gruvbox\").unwrap();\n        Styles::from_theme(\"gruvbox-light\").unwrap();\n        Styles::from_theme(\"nord\").unwrap();\n        Styles::from_theme(\"nord-light\").unwrap();\n    }\n}\n"
  },
  {
    "path": "src/options/config/temperature.rs",
    "content": "use serde::Deserialize;\n\nuse super::IgnoreList;\n\n/// Temperature configuration.\n#[derive(Clone, Debug, Default, Deserialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub(crate) struct TempConfig {\n    /// A filter over the sensor names.\n    pub(crate) sensor_filter: Option<IgnoreList>,\n}\n"
  },
  {
    "path": "src/options/config.rs",
    "content": "pub mod cpu;\npub mod disk;\npub mod flags;\nmod ignore_list;\npub mod layout;\npub mod network;\npub mod process;\npub mod style;\npub mod temperature;\n\nuse disk::DiskConfig;\nuse flags::GeneralConfig;\nuse network::NetworkConfig;\nuse serde::{Deserialize, Serialize};\nuse style::StyleConfig;\nuse temperature::TempConfig;\n\npub use self::ignore_list::IgnoreList;\nuse self::{cpu::CpuConfig, layout::Row, process::ProcessesConfig};\n\n/// Overall config for `bottom`.\n#[derive(Clone, Debug, Default, Deserialize)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]\npub struct Config {\n    pub(crate) flags: Option<GeneralConfig>,\n    pub(crate) styles: Option<StyleConfig>,\n    pub(crate) row: Option<Vec<Row>>,\n    pub(crate) processes: Option<ProcessesConfig>,\n    pub(crate) disk: Option<DiskConfig>,\n    pub(crate) temperature: Option<TempConfig>,\n    pub(crate) network: Option<NetworkConfig>,\n    pub(crate) cpu: Option<CpuConfig>,\n}\n\n#[derive(Clone, Debug, Deserialize, Serialize)]\n#[serde(untagged)]\n#[cfg_attr(feature = \"generate_schema\", derive(schemars::JsonSchema))]\n#[cfg_attr(test, derive(PartialEq, Eq))]\npub(crate) enum StringOrNum {\n    String(String),\n    Num(u64),\n}\n\nimpl From<String> for StringOrNum {\n    fn from(value: String) -> Self {\n        StringOrNum::String(value)\n    }\n}\n\nimpl From<u64> for StringOrNum {\n    fn from(value: u64) -> Self {\n        StringOrNum::Num(value)\n    }\n}\n\n#[cfg(test)]\nmod test {\n\n    // Test all valid configs in the integration test folder and ensure they are accepted.\n    // We need this separated as only test library code sets `serde(deny_unknown_fields)`.\n    #[test]\n    #[cfg(feature = \"default\")]\n    fn test_integration_valid_configs() {\n        use std::fs;\n\n        use super::Config;\n\n        for config_path in fs::read_dir(\"./tests/valid_configs\").unwrap() {\n            let dir_entry = config_path.unwrap();\n            let path = dir_entry.path();\n\n            if path.is_file() {\n                let config_path_str = path.display().to_string();\n                let config_str = fs::read_to_string(path).unwrap();\n\n                toml_edit::de::from_str::<Config>(&config_str)\n                    .unwrap_or_else(|_| panic!(\"incorrectly rejected '{config_path_str}'\"));\n            }\n        }\n    }\n\n    // I didn't do an invalid config test as a lot of them _are_ valid Config when parsed,\n    // but fail other checks.\n}\n"
  },
  {
    "path": "src/options/error.rs",
    "content": "use std::borrow::Cow;\n\n/// An error around some option-setting, and the reason.\n///\n/// These are meant to potentially be user-facing (e.g. explain\n/// why it's broken and what to fix), and as so treat it as such!\n///\n/// For stylistic and consistency reasons, use _single quotes_ (e.g. `'bad'`)\n/// for highlighting error values. You can use (\".*`.+`.*\") as a regex to check\n/// for this.\n#[derive(Debug, PartialEq)]\npub enum OptionError {\n    Config(Cow<'static, str>),\n    Argument(Cow<'static, str>),\n    Other(Cow<'static, str>),\n}\n\nimpl OptionError {\n    /// Create a new [`OptionError::Config`].\n    pub(crate) fn config<R: Into<Cow<'static, str>>>(reason: R) -> Self {\n        OptionError::Config(reason.into())\n    }\n\n    /// Create a new [`OptionError::Config`] for an invalid value.\n    pub(crate) fn invalid_config_value(value: &str) -> Self {\n        OptionError::Config(Cow::Owned(format!(\n            \"'{value}' was set with an invalid value, please update it in your config file.\"\n        )))\n    }\n\n    /// Create a new [`OptionError::Argument`].\n    pub(crate) fn arg<R: Into<Cow<'static, str>>>(reason: R) -> Self {\n        OptionError::Argument(reason.into())\n    }\n\n    /// Create a new [`OptionError::Argument`] for an invalid value.\n    pub(crate) fn invalid_arg_value(value: &str) -> Self {\n        OptionError::Argument(Cow::Owned(format!(\n            \"'--{value}' was set with an invalid value, please update your arguments.\"\n        )))\n    }\n\n    /// Create a new [`OptionError::Other`].\n    pub(crate) fn other<R: Into<Cow<'static, str>>>(reason: R) -> Self {\n        OptionError::Other(reason.into())\n    }\n}\n\npub(crate) type OptionResult<T> = Result<T, OptionError>;\n\nimpl std::fmt::Display for OptionError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            OptionError::Config(reason) => write!(f, \"Configuration file error: {reason}\"),\n            OptionError::Argument(reason) => write!(f, \"Argument error: {reason}\"),\n            OptionError::Other(reason) => {\n                write!(f, \"Error with the config file or the arguments: {reason}\")\n            }\n        }\n    }\n}\n\nimpl std::error::Error for OptionError {}\n\nimpl From<toml_edit::de::Error> for OptionError {\n    fn from(err: toml_edit::de::Error) -> Self {\n        OptionError::Config(err.to_string().into())\n    }\n}\n\nimpl From<std::io::Error> for OptionError {\n    fn from(err: std::io::Error) -> Self {\n        OptionError::Other(err.to_string().into())\n    }\n}\n"
  },
  {
    "path": "src/options.rs",
    "content": "//! How to handle config files and arguments.\n\n// TODO: Break this apart or do something a bit smarter.\n\npub mod args;\npub mod config;\nmod error;\n\nuse std::{\n    convert::TryInto,\n    fs,\n    io::Write,\n    path::{Path, PathBuf},\n    str::FromStr,\n    time::{Duration, Instant},\n};\n\nuse anyhow::{Context, Result};\npub use config::Config;\nuse config::style::Styles;\nuse data::TemperatureType;\npub(crate) use error::{OptionError, OptionResult};\nuse indexmap::IndexSet;\nuse regex::Regex;\nuse rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};\n#[cfg(feature = \"battery\")]\nuse starship_battery::Manager;\n\nuse self::{\n    args::BottomArgs,\n    config::{IgnoreList, StringOrNum, layout::Row},\n};\nuse crate::{\n    app::{filter::Filter, layout_manager::*, *},\n    canvas::components::time_graph::LegendPosition,\n    constants::*,\n    utils::data_units::DataUnit,\n    widgets::*,\n};\n\nmacro_rules! is_flag_enabled {\n    ($flag_name:ident, $arg:expr, $config:expr) => {\n        if $arg.$flag_name {\n            true\n        } else if let Some(flags) = &$config.flags {\n            flags.$flag_name.unwrap_or(false)\n        } else {\n            false\n        }\n    };\n\n    ($cmd_flag:literal, $cfg_flag:ident, $matches:expr, $config:expr) => {\n        if $matches.get_flag($cmd_flag) {\n            true\n        } else if let Some(flags) = &$config.flags {\n            flags.$cfg_flag.unwrap_or(false)\n        } else {\n            false\n        }\n    };\n}\n\n/// A new version if [`is_flag_enabled`] which instead expects the user to pass in `config_section`, which is\n/// the section the flag is located, rather than defaulting to `config.flags` where `config` is passed in.\nmacro_rules! is_flag_enabled_new {\n    ($flag_name:ident, $arg:expr, $config_section:expr) => {\n        if $arg.$flag_name {\n            true\n        } else if let Some(options) = &$config_section {\n            options.$flag_name.unwrap_or(false)\n        } else {\n            false\n        }\n    };\n}\n\n/// The default config file sub-path.\nconst DEFAULT_CONFIG_FILE_LOCATION: &str = \"bottom/bottom.toml\";\n\n/// Returns the config path to use. If `override_config_path` is specified, then\n/// we will use that. If not, then return the \"default\" config path, which is:\n///\n/// - If a path already exists at `<HOME>/bottom/bottom.toml`, then use that for\n///   legacy reasons.\n/// - Otherwise, use `<SYSTEM_CONFIG_FOLDER>/bottom/bottom.toml`.\n///\n/// For more details on this, see [dirs](https://docs.rs/dirs/latest/dirs/fn.config_dir.html)'\n/// documentation.\n///\n/// XXX: For macOS, we additionally will manually check `$XDG_CONFIG_HOME` as well first\n/// before falling back to `dirs`.\nfn get_config_path(override_config_path: Option<&Path>) -> Option<PathBuf> {\n    if let Some(conf_loc) = override_config_path {\n        return Some(conf_loc.to_path_buf());\n    } else if let Some(home_path) = dirs::home_dir() {\n        let mut old_home_path = home_path;\n        old_home_path.push(\".config/\");\n        old_home_path.push(DEFAULT_CONFIG_FILE_LOCATION);\n        if let Ok(res) = old_home_path.try_exists() {\n            if res {\n                // We used to create it at `<HOME>/DEFAULT_CONFIG_FILE_PATH`, but changed it\n                // to be more correct later. However, for legacy reasons, if it already exists,\n                // use the old one.\n                return Some(old_home_path);\n            }\n        }\n    }\n\n    let config_path = dirs::config_dir().map(|mut path| {\n        path.push(DEFAULT_CONFIG_FILE_LOCATION);\n        path\n    });\n\n    if cfg!(target_os = \"macos\") {\n        if let Ok(xdg_config_path) = std::env::var(\"XDG_CONFIG_HOME\") {\n            if !xdg_config_path.is_empty() {\n                // If XDG_CONFIG_HOME exists and is non-empty, _but_ we previously used the Library-based path\n                // for a config and it exists, then use that instead for backwards-compatibility.\n                if let Some(old_macos_path) = &config_path {\n                    if let Ok(res) = old_macos_path.try_exists() {\n                        if res {\n                            return config_path;\n                        }\n                    }\n                }\n\n                // Otherwise, try and use the XDG_CONFIG_HOME-based path.\n                let mut cfg_path = PathBuf::new();\n                cfg_path.push(xdg_config_path);\n                cfg_path.push(DEFAULT_CONFIG_FILE_LOCATION);\n\n                return Some(cfg_path);\n            }\n        }\n    }\n\n    config_path\n}\n\nfn create_config_at_path(path: &Path) -> anyhow::Result<Config> {\n    if let Some(parent_path) = path.parent() {\n        fs::create_dir_all(parent_path)?;\n    }\n\n    let mut file = fs::File::create(path)?;\n    file.write_all(CONFIG_TEXT.as_bytes())?;\n\n    Ok(Config::default())\n}\n\n/// Get the config at `config_path`. If there is no config file at the specified\n/// path, it will try to create a new file with the default settings, and return\n/// the default config.\n///\n/// We're going to use the following behaviour on when we'll return an error rather\n/// than just \"silently\" continuing on:\n/// - If the user passed in a path explicitly, then we will be loud and error out.\n/// - If the user does NOT pass in a path explicitly, then just show a warning,\n///   but continue. This is in case they do not want to write a default config file at\n///   the XDG locations, for example.\npub(crate) fn get_or_create_config(config_path: Option<&Path>) -> anyhow::Result<Config> {\n    let adjusted_config_path = get_config_path(config_path);\n\n    match &adjusted_config_path {\n        Some(path) => {\n            if let Ok(config_string) = fs::read_to_string(path) {\n                Ok(toml_edit::de::from_str(&config_string)?)\n            } else {\n                match create_config_at_path(path) {\n                    Ok(cfg) => Ok(cfg),\n                    Err(err) => {\n                        if config_path.is_some() {\n                            Err(err.context(format!(\n                                \"bottom could not create a new config file at '{}'.\",\n                                path.display()\n                            )))\n                        } else {\n                            indoc::eprintdoc!(\n                                \"Note: bottom couldn't create a default config file at '{}', and the \\\n                                application has fallen back to the default configuration.\n                                    \n                                Caused by:\n                                    {err}\n                                \",\n                                path.display()\n                            );\n\n                            Ok(Config::default())\n                        }\n                    }\n                }\n            }\n        }\n        None => {\n            // If we somehow don't have any config path, then just assume the default config\n            // but don't write to any file.\n            //\n            // TODO: For now, just print a message to stderr indicating this. In the future,\n            // probably show in-app (too).\n\n            eprintln!(\n                \"Note: bottom couldn't find a location to create or read a config file, so \\\n                the application has fallen back to the default configuration. \\\n                This could be for a variety of reasons, such as issues with file permissions.\"\n            );\n\n            Ok(Config::default())\n        }\n    }\n}\n\n/// Initialize the app.\npub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomLayout, Styles)> {\n    use BottomWidgetType::*;\n\n    // Since everything takes a reference, but we want to take ownership here to\n    // drop matches/config later...\n    let args = &args;\n    let config = &config;\n\n    let styling = Styles::new(args, config)?;\n\n    let (widget_layout, default_widget_id, default_widget_type_option) =\n        get_widget_layout(args, config)\n            .context(\"Found an issue while trying to build the widget layout.\")?;\n\n    let retention_ms = get_retention(args, config)?;\n    let autohide_time = is_flag_enabled!(autohide_time, args.general, config);\n    let default_time_value = get_default_time_value(args, config, retention_ms)?;\n\n    let use_basic_mode = is_flag_enabled!(basic, args.general, config);\n    let expanded = is_flag_enabled!(expanded, args.general, config);\n    #[cfg(feature = \"zfs\")]\n    let free_arc = is_flag_enabled!(free_arc, args.memory, config);\n\n    // For processes\n    let is_grouped = is_flag_enabled!(group_processes, args.process, config);\n    let is_case_sensitive = is_flag_enabled!(case_sensitive, args.process, config);\n    let is_match_whole_word = is_flag_enabled!(whole_word, args.process, config);\n    let is_use_regex = is_flag_enabled!(regex, args.process, config);\n    let is_default_tree = is_flag_enabled!(tree, args.process, config);\n    let is_default_command = is_flag_enabled!(process_command, args.process, config);\n    #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n    let is_advanced_kill = !(is_flag_enabled!(disable_advanced_kill, args.process, config));\n    let is_read_only = is_flag_enabled!(read_only, args.process, config);\n    #[cfg(target_os = \"linux\")]\n    let hide_k_threads = is_flag_enabled!(hide_k_threads, args.process, config);\n\n    let process_memory_as_value = is_flag_enabled!(process_memory_as_value, args.process, config);\n    let is_default_tree_collapsed = is_flag_enabled!(tree_collapse, args.process, config);\n\n    // For CPU\n    let default_cpu_selection = get_default_cpu_selection(args, config);\n\n    let mut widget_map = HashMap::default();\n    let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::default();\n    let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::default();\n    let mut net_state_map: HashMap<u64, NetWidgetState> = HashMap::default();\n    let mut proc_state_map: HashMap<u64, ProcWidgetState> = HashMap::default();\n    let mut temp_state_map: HashMap<u64, TempWidgetState> = HashMap::default();\n    let mut disk_state_map: HashMap<u64, DiskTableWidget> = HashMap::default();\n    let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::default();\n\n    let autohide_timer = if autohide_time {\n        Some(Instant::now())\n    } else {\n        None\n    };\n\n    let mut initial_widget_id: u64 = default_widget_id;\n    let mut initial_widget_type = Proc;\n    let is_custom_layout = config.row.is_some();\n    let mut used_widget_set = HashSet::default();\n\n    let network_unit_type = get_network_unit_type(args, config);\n    let network_scale_type = get_network_scale_type(args, config);\n    let network_use_binary_prefix =\n        is_flag_enabled!(network_use_binary_prefix, args.network, config);\n    let network_show_packets = get_network_show_packets(args, config);\n\n    let proc_columns: Option<IndexSet<ProcWidgetColumn>> = {\n        config.processes.as_ref().and_then(|cfg| {\n            if cfg.columns.is_empty() {\n                None\n            } else {\n                // TODO: Should we be using an indexmap? Or maybe allow dupes.\n                Some(IndexSet::from_iter(\n                    cfg.columns.iter().map(ProcWidgetColumn::from),\n                ))\n            }\n        })\n    };\n\n    let network_legend_position = get_network_legend_position(args, config)?;\n    let memory_legend_position = get_memory_legend_position(args, config)?;\n\n    // TODO: Can probably just reuse the options struct.\n    let app_config_fields = AppConfigFields {\n        update_rate: get_update_rate(args, config)?,\n        temperature_type: get_temperature(args, config)\n            .context(\"Update 'temperature_type' in your config file.\")?,\n        show_average_cpu: get_show_average_cpu(args, config),\n        use_dot: is_flag_enabled!(dot_marker, args.general, config),\n        cpu_left_legend: is_flag_enabled!(cpu_left_legend, args.cpu, config),\n        use_current_cpu_total: is_flag_enabled!(current_usage, args.process, config),\n        unnormalized_cpu: is_flag_enabled!(unnormalized_cpu, args.process, config),\n        get_process_threads: is_flag_enabled_new!(get_threads, args.process, config.processes),\n        use_basic_mode,\n        default_time_value,\n        time_interval: get_time_interval(args, config, retention_ms)?,\n        hide_time: is_flag_enabled!(hide_time, args.general, config),\n        autohide_time,\n        use_old_network_legend: is_flag_enabled!(use_old_network_legend, args.network, config),\n        table_gap: u16::from(!(is_flag_enabled!(hide_table_gap, args.general, config))),\n        disable_click: is_flag_enabled!(disable_click, args.general, config),\n        disable_keys: is_flag_enabled!(disable_keys, args.general, config),\n        enable_gpu: get_enable_gpu(args, config),\n        enable_cache_memory: get_enable_cache_memory(args, config),\n        show_table_scroll_position: is_flag_enabled!(\n            show_table_scroll_position,\n            args.general,\n            config\n        ),\n        #[cfg(any(target_os = \"linux\", target_os = \"macos\", target_os = \"freebsd\"))]\n        is_advanced_kill,\n        is_read_only,\n        #[cfg(target_os = \"linux\")]\n        hide_k_threads,\n        memory_legend_position,\n        network_legend_position,\n        network_scale_type,\n        network_unit_type,\n        network_use_binary_prefix,\n        network_show_packets,\n        retention_ms,\n        dedicated_average_row: get_dedicated_avg_row(config),\n        default_tree_collapse: is_default_tree_collapsed,\n        #[cfg(feature = \"zfs\")]\n        free_arc,\n    };\n\n    let table_config = ProcTableConfig {\n        is_case_sensitive,\n        is_match_whole_word,\n        is_use_regex,\n        show_memory_as_values: process_memory_as_value,\n        is_command: is_default_command,\n    };\n\n    for row in &widget_layout.rows {\n        for col in &row.children {\n            for col_row in &col.children {\n                for widget in &col_row.children {\n                    widget_map.insert(widget.widget_id, widget.clone());\n                    if let Some(default_widget_type) = &default_widget_type_option {\n                        if !is_custom_layout || use_basic_mode {\n                            match widget.widget_type {\n                                BasicCpu => {\n                                    if let Cpu = *default_widget_type {\n                                        initial_widget_id = widget.widget_id;\n                                        initial_widget_type = Cpu;\n                                    }\n                                }\n                                BasicMem => {\n                                    if let Mem = *default_widget_type {\n                                        initial_widget_id = widget.widget_id;\n                                        initial_widget_type = Cpu;\n                                    }\n                                }\n                                BasicNet => {\n                                    if let Net = *default_widget_type {\n                                        initial_widget_id = widget.widget_id;\n                                        initial_widget_type = Cpu;\n                                    }\n                                }\n                                _ => {\n                                    if *default_widget_type == widget.widget_type {\n                                        initial_widget_id = widget.widget_id;\n                                        initial_widget_type = widget.widget_type.clone();\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    used_widget_set.insert(widget.widget_type.clone());\n\n                    match widget.widget_type {\n                        Cpu => {\n                            cpu_state_map.insert(\n                                widget.widget_id,\n                                CpuWidgetState::new(\n                                    &app_config_fields,\n                                    default_cpu_selection,\n                                    default_time_value,\n                                    autohide_timer,\n                                    &styling,\n                                ),\n                            );\n                        }\n                        Mem => {\n                            mem_state_map.insert(\n                                widget.widget_id,\n                                MemWidgetState::init(default_time_value, autohide_timer),\n                            );\n                        }\n                        Net => {\n                            net_state_map.insert(\n                                widget.widget_id,\n                                NetWidgetState::init(default_time_value, autohide_timer),\n                            );\n                        }\n                        Proc => {\n                            let mode = if is_grouped {\n                                ProcWidgetMode::Grouped\n                            } else if is_default_tree {\n                                ProcWidgetMode::Tree(TreeCollapsed::new(is_default_tree_collapsed))\n                            } else {\n                                ProcWidgetMode::Normal\n                            };\n\n                            proc_state_map.insert(\n                                widget.widget_id,\n                                ProcWidgetState::new(\n                                    &app_config_fields,\n                                    mode,\n                                    table_config,\n                                    &styling,\n                                    &proc_columns,\n                                ),\n                            );\n                        }\n                        Disk => {\n                            disk_state_map.insert(\n                                widget.widget_id,\n                                DiskTableWidget::new(\n                                    &app_config_fields,\n                                    &styling,\n                                    config.disk.as_ref().and_then(|cfg| cfg.columns.as_deref()),\n                                ),\n                            );\n                        }\n                        Temp => {\n                            temp_state_map.insert(\n                                widget.widget_id,\n                                TempWidgetState::new(&app_config_fields, &styling),\n                            );\n                        }\n                        Battery => {\n                            battery_state_map\n                                .insert(widget.widget_id, BatteryWidgetState::default());\n                        }\n                        _ => {}\n                    }\n                }\n            }\n        }\n    }\n\n    let basic_table_widget_state = if use_basic_mode {\n        Some(match initial_widget_type {\n            Proc | Disk | Temp => BasicTableWidgetState {\n                currently_displayed_widget_type: initial_widget_type,\n                currently_displayed_widget_id: initial_widget_id,\n                left_tlc: None,\n                left_brc: None,\n                right_tlc: None,\n                right_brc: None,\n            },\n            _ => BasicTableWidgetState {\n                currently_displayed_widget_type: Proc,\n                currently_displayed_widget_id: DEFAULT_WIDGET_ID,\n                left_tlc: None,\n                left_brc: None,\n                right_tlc: None,\n                right_brc: None,\n            },\n        })\n    } else {\n        None\n    };\n\n    let use_mem = used_widget_set.contains(&Mem) || used_widget_set.contains(&BasicMem);\n    let used_widgets = UsedWidgets {\n        use_cpu: used_widget_set.contains(&Cpu) || used_widget_set.contains(&BasicCpu),\n        use_mem,\n        use_cache: use_mem && get_enable_cache_memory(args, config),\n        use_gpu: get_enable_gpu(args, config),\n        use_net: used_widget_set.contains(&Net) || used_widget_set.contains(&BasicNet),\n        use_proc: used_widget_set.contains(&Proc),\n        use_disk: used_widget_set.contains(&Disk),\n        use_temp: used_widget_set.contains(&Temp),\n        use_battery: used_widget_set.contains(&Battery),\n    };\n\n    let (disk_name_filter, disk_mount_filter) = {\n        match &config.disk {\n            Some(cfg) => {\n                let df = get_ignore_list(&cfg.name_filter)\n                    .context(\"Update 'disk.name_filter' in your config file\")?;\n                let mf = get_ignore_list(&cfg.mount_filter)\n                    .context(\"Update 'disk.mount_filter' in your config file\")?;\n\n                (df, mf)\n            }\n            None => (None, None),\n        }\n    };\n    let temp_sensor_filter = match &config.temperature {\n        Some(cfg) => get_ignore_list(&cfg.sensor_filter)\n            .context(\"Update 'temperature.sensor_filter' in your config file\")?,\n        None => None,\n    };\n    let net_interface_filter = match &config.network {\n        Some(cfg) => get_ignore_list(&cfg.interface_filter)\n            .context(\"Update 'network.interface_filter' in your config file\")?,\n        None => None,\n    };\n\n    let states = AppWidgetStates {\n        cpu_state: CpuState::init(cpu_state_map),\n        mem_state: MemState::init(mem_state_map),\n        net_state: NetState::init(net_state_map),\n        proc_state: ProcState::init(proc_state_map),\n        temp_state: TempState::init(temp_state_map),\n        disk_state: DiskState::init(disk_state_map),\n        battery_state: AppBatteryState::init(battery_state_map),\n        basic_table_widget_state,\n    };\n\n    let current_widget = widget_map\n        .get(&initial_widget_id)\n        .expect(\"widget map should have the initial widget ID\")\n        .clone();\n    let filters = DataFilters {\n        disk_filter: disk_name_filter,\n        mount_filter: disk_mount_filter,\n        temp_filter: temp_sensor_filter,\n        net_filter: net_interface_filter,\n    };\n    let is_expanded = expanded && !use_basic_mode;\n\n    Ok((\n        App::new(\n            app_config_fields,\n            states,\n            widget_map,\n            current_widget,\n            used_widgets,\n            filters,\n            is_expanded,\n        ),\n        widget_layout,\n        styling,\n    ))\n}\n\nfn get_widget_layout(\n    args: &BottomArgs, config: &Config,\n) -> OptionResult<(BottomLayout, u64, Option<BottomWidgetType>)> {\n    let cpu_left_legend = is_flag_enabled!(cpu_left_legend, args.cpu, config);\n\n    let (default_widget_type, mut default_widget_count) =\n        get_default_widget_and_count(args, config)?;\n    let mut default_widget_id = 1;\n\n    let bottom_layout = if is_flag_enabled!(basic, args.general, config) {\n        default_widget_id = DEFAULT_WIDGET_ID;\n\n        BottomLayout::init_basic_default(get_use_battery(args, config))\n    } else {\n        let ref_row: Vec<Row>; // Required to handle reference\n        let rows = match &config.row {\n            Some(r) => r,\n            None => {\n                ref_row = toml_edit::de::from_str::<Config>(if get_use_battery(args, config) {\n                    DEFAULT_BATTERY_LAYOUT\n                } else {\n                    DEFAULT_LAYOUT\n                })?\n                .row\n                .expect(\"This cannot (like it really shouldn't) fail!\");\n                &ref_row\n            }\n        };\n\n        let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs*\n        let mut total_height_ratio = 0;\n\n        let mut ret_bottom_layout = BottomLayout {\n            rows: rows\n                .iter()\n                .map(|row| {\n                    row.convert_row_to_bottom_row(\n                        &mut iter_id,\n                        &mut total_height_ratio,\n                        &mut default_widget_id,\n                        &default_widget_type,\n                        &mut default_widget_count,\n                        cpu_left_legend,\n                    )\n                    .map_err(|err| OptionError::config(err.to_string()))\n                })\n                .collect::<OptionResult<Vec<_>>>()?,\n            total_row_height_ratio: total_height_ratio,\n        };\n\n        // Confirm that we have at least ONE widget left - if not, error out!\n        if iter_id > 0 {\n            ret_bottom_layout.get_movement_mappings();\n            ret_bottom_layout\n        } else {\n            return Err(OptionError::config(\n                \"have at least one widget under the '[[row]]' section.\",\n            ));\n        }\n    };\n\n    Ok((bottom_layout, default_widget_id, default_widget_type))\n}\n\n#[inline]\nfn try_parse_ms(s: &str) -> Result<u64, ()> {\n    Ok(if let Ok(val) = humantime::parse_duration(s) {\n        val.as_millis().try_into().map_err(|_| ())?\n    } else if let Ok(val) = s.parse::<u64>() {\n        val\n    } else {\n        return Err(());\n    })\n}\n\nmacro_rules! parse_arg_value {\n    ($to_try:expr, $flag:literal) => {\n        $to_try.map_err(|_| OptionError::invalid_arg_value($flag))\n    };\n}\n\nmacro_rules! parse_config_value {\n    ($to_try:expr, $setting:literal) => {\n        $to_try.map_err(|_| OptionError::invalid_config_value($setting))\n    };\n}\n\nmacro_rules! parse_ms_option {\n    ($arg_expr:expr, $config_expr:expr, $default_value:expr, $setting:literal, $low:expr, $high:expr $(,)?) => {{\n        use humantime::format_duration;\n\n        if let Some(to_parse) = $arg_expr {\n            let value = parse_arg_value!(try_parse_ms(to_parse), $setting)?;\n\n            if let Some(limit) = $low {\n                if value < limit {\n                    return Err(OptionError::arg(format!(\n                        \"'--{}' must be greater than {}\",\n                        $setting,\n                        format_duration(Duration::from_millis(limit))\n                    )));\n                }\n            }\n\n            if let Some(limit) = $high {\n                if value > limit {\n                    return Err(OptionError::arg(format!(\n                        \"'--{}' must be less than {}\",\n                        $setting,\n                        format_duration(Duration::from_millis(limit))\n                    )));\n                }\n            }\n\n            Ok(value)\n        } else if let Some(to_parse) = $config_expr {\n            let value = match to_parse {\n                StringOrNum::String(s) => parse_config_value!(try_parse_ms(s), $setting)?,\n                StringOrNum::Num(n) => *n,\n            };\n\n            if let Some(limit) = $low {\n                if value < limit {\n                    return Err(OptionError::arg(format!(\n                        \"'{}' must be greater than {}\",\n                        $setting,\n                        format_duration(Duration::from_millis(limit))\n                    )));\n                }\n            }\n\n            if let Some(limit) = $high {\n                if value > limit {\n                    return Err(OptionError::arg(format!(\n                        \"'{}' must be less than {}\",\n                        $setting,\n                        format_duration(Duration::from_millis(limit))\n                    )));\n                }\n            }\n\n            Ok(value)\n        } else {\n            Ok($default_value)\n        }\n    }};\n}\n\n/// How quickly we update data.\n#[inline]\nfn get_update_rate(args: &BottomArgs, config: &Config) -> OptionResult<u64> {\n    const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u64 = 1000;\n\n    parse_ms_option!(\n        &args.general.rate,\n        config.flags.as_ref().and_then(|flags| flags.rate.as_ref()),\n        DEFAULT_REFRESH_RATE_IN_MILLISECONDS,\n        \"rate\",\n        Some(250),\n        None,\n    )\n}\n\nfn get_temperature(args: &BottomArgs, config: &Config) -> OptionResult<TemperatureType> {\n    if args.temperature.fahrenheit {\n        return Ok(TemperatureType::Fahrenheit);\n    } else if args.temperature.kelvin {\n        return Ok(TemperatureType::Kelvin);\n    } else if args.temperature.celsius {\n        return Ok(TemperatureType::Celsius);\n    } else if let Some(flags) = &config.flags {\n        if let Some(temp_type) = &flags.temperature_type {\n            return parse_config_value!(TemperatureType::from_str(temp_type), \"temperature_type\");\n        }\n    }\n    Ok(TemperatureType::Celsius)\n}\n\n/// Yes, this function gets whether to show average CPU (true) or not (false).\nfn get_show_average_cpu(args: &BottomArgs, config: &Config) -> bool {\n    if args.cpu.hide_avg_cpu {\n        return false;\n    } else if let Some(flags) = &config.flags {\n        if let Some(avg_cpu) = flags.hide_avg_cpu {\n            return !avg_cpu;\n        }\n    }\n\n    true\n}\n\n// I hate this too.\nfn get_default_cpu_selection(args: &BottomArgs, config: &Config) -> config::cpu::CpuDefault {\n    match &args.cpu.default_cpu_entry {\n        Some(default) => match default {\n            args::CpuDefault::All => config::cpu::CpuDefault::All,\n            args::CpuDefault::Average => config::cpu::CpuDefault::Average,\n        },\n        None => config.cpu.as_ref().map(|c| c.default).unwrap_or_default(),\n    }\n}\n\nfn get_dedicated_avg_row(config: &Config) -> bool {\n    config\n        .flags\n        .as_ref()\n        .and_then(|flags| flags.average_cpu_row)\n        .unwrap_or(false)\n}\n\n#[inline]\nfn get_default_time_value(\n    args: &BottomArgs, config: &Config, retention_ms: u64,\n) -> OptionResult<u64> {\n    const DEFAULT_TIME_MILLISECONDS: u64 = 60 * 1000; // Defaults to 1 min.\n\n    parse_ms_option!(\n        &args.general.default_time_value,\n        config\n            .flags\n            .as_ref()\n            .and_then(|flags| flags.default_time_value.as_ref()),\n        DEFAULT_TIME_MILLISECONDS,\n        \"default_time_value\",\n        Some(30000),\n        Some(retention_ms),\n    )\n}\n\n#[inline]\nfn get_time_interval(args: &BottomArgs, config: &Config, retention_ms: u64) -> OptionResult<u64> {\n    const TIME_CHANGE_MILLISECONDS: u64 = 15 * 1000; // How much to increment each time\n\n    parse_ms_option!(\n        &args.general.time_delta,\n        config\n            .flags\n            .as_ref()\n            .and_then(|flags| flags.time_delta.as_ref()),\n        TIME_CHANGE_MILLISECONDS,\n        \"time_delta\",\n        Some(1000),\n        Some(retention_ms),\n    )\n}\n\nfn get_default_widget_and_count(\n    args: &BottomArgs, config: &Config,\n) -> OptionResult<(Option<BottomWidgetType>, u64)> {\n    let widget_type = if let Some(widget_type) = &args.general.default_widget_type {\n        let parsed_widget = parse_arg_value!(widget_type.parse(), \"default_widget_type\")?;\n        if let BottomWidgetType::Empty = parsed_widget {\n            None\n        } else {\n            Some(parsed_widget)\n        }\n    } else if let Some(flags) = &config.flags {\n        if let Some(widget_type) = &flags.default_widget_type {\n            let parsed_widget = parse_config_value!(widget_type.parse(), \"default_widget_type\")?;\n            if let BottomWidgetType::Empty = parsed_widget {\n                None\n            } else {\n                Some(parsed_widget)\n            }\n        } else {\n            None\n        }\n    } else {\n        None\n    };\n\n    let widget_count: Option<u128> = if let Some(widget_count) = args.general.default_widget_count {\n        Some(widget_count.into())\n    } else {\n        config.flags.as_ref().and_then(|flags| {\n            flags\n                .default_widget_count\n                .map(|widget_count| widget_count.into())\n        })\n    };\n\n    match (widget_type, widget_count) {\n        (Some(widget_type), Some(widget_count)) => {\n            let widget_count = widget_count.try_into().map_err(|_| OptionError::other(\n                \"set your widget count to be at most 18446744073709551615.\".to_string()\n            ))?;\n            Ok((Some(widget_type), widget_count))\n        }\n        (Some(widget_type), None) => Ok((Some(widget_type), 1)),\n        (None, Some(_widget_count)) =>  Err(OptionError::other(\n            \"cannot set 'default_widget_count' by itself, it must be used with 'default_widget_type'.\".to_string(),\n        )),\n        (None, None) => Ok((None, 1))\n    }\n}\n\n#[cfg(feature = \"battery\")]\nfn get_use_battery(args: &BottomArgs, config: &Config) -> bool {\n    // TODO: Move this so it's dynamic in the app itself and automatically hide if\n    // there are no batteries?\n    if let Ok(battery_manager) = Manager::new() {\n        if let Ok(batteries) = battery_manager.batteries() {\n            if batteries.count() == 0 {\n                return false;\n            }\n        }\n    }\n\n    if args.battery.battery {\n        return true;\n    } else if let Some(flags) = &config.flags {\n        if let Some(battery) = flags.battery {\n            return battery;\n        }\n    }\n\n    false\n}\n\n#[cfg(not(feature = \"battery\"))]\nfn get_use_battery(_args: &BottomArgs, _config: &Config) -> bool {\n    false\n}\n\n#[cfg(feature = \"gpu\")]\nfn get_enable_gpu(args: &BottomArgs, config: &Config) -> bool {\n    if args.gpu.disable_gpu {\n        return false;\n    }\n\n    !config\n        .flags\n        .as_ref()\n        .and_then(|f| f.disable_gpu)\n        .unwrap_or(false)\n}\n\n#[cfg(not(feature = \"gpu\"))]\nfn get_enable_gpu(_: &BottomArgs, _: &Config) -> bool {\n    false\n}\n\n#[cfg(not(target_os = \"windows\"))]\nfn get_enable_cache_memory(args: &BottomArgs, config: &Config) -> bool {\n    if args.memory.enable_cache_memory {\n        return true;\n    } else if let Some(flags) = &config.flags {\n        if let Some(enable_cache_memory) = flags.enable_cache_memory {\n            return enable_cache_memory;\n        }\n    }\n\n    false\n}\n\n#[cfg(target_os = \"windows\")]\nfn get_enable_cache_memory(_args: &BottomArgs, _config: &Config) -> bool {\n    false\n}\n\nfn get_ignore_list(ignore_list: &Option<IgnoreList>) -> OptionResult<Option<Filter>> {\n    if let Some(ignore_list) = ignore_list {\n        let list: Result<Vec<_>, _> = ignore_list\n            .list\n            .iter()\n            .map(|name| {\n                let escaped_string: String;\n                let res = format!(\n                    \"{}{}{}{}\",\n                    if ignore_list.whole_word { \"^\" } else { \"\" },\n                    if ignore_list.case_sensitive {\n                        \"\"\n                    } else {\n                        \"(?i)\"\n                    },\n                    if ignore_list.regex {\n                        name\n                    } else {\n                        escaped_string = regex::escape(name);\n                        &escaped_string\n                    },\n                    if ignore_list.whole_word { \"$\" } else { \"\" },\n                );\n\n                Regex::new(&res)\n            })\n            .collect();\n\n        let list = list.map_err(|err| OptionError::config(err.to_string()))?;\n\n        Ok(Some(Filter::new(ignore_list.is_list_ignored, list)))\n    } else {\n        Ok(None)\n    }\n}\n\nfn get_network_unit_type(args: &BottomArgs, config: &Config) -> DataUnit {\n    if args.network.network_use_bytes {\n        return DataUnit::Byte;\n    } else if let Some(flags) = &config.flags {\n        if let Some(network_use_bytes) = flags.network_use_bytes {\n            if network_use_bytes {\n                return DataUnit::Byte;\n            }\n        }\n    }\n\n    DataUnit::Bit\n}\n\nfn get_network_scale_type(args: &BottomArgs, config: &Config) -> AxisScaling {\n    if args.network.network_use_log {\n        return AxisScaling::Log;\n    } else if let Some(flags) = &config.flags {\n        if let Some(network_use_log) = flags.network_use_log {\n            if network_use_log {\n                return AxisScaling::Log;\n            }\n        }\n    }\n\n    AxisScaling::Linear\n}\n\nfn get_network_show_packets(args: &BottomArgs, config: &Config) -> bool {\n    if args.network.show_packets {\n        return true;\n    } else if let Some(network_config) = &config.network {\n        if let Some(show_packets) = network_config.show_packets {\n            return show_packets;\n        }\n    }\n\n    false\n}\n\nfn get_retention(args: &BottomArgs, config: &Config) -> OptionResult<u64> {\n    const DEFAULT_RETENTION_MS: u64 = 600 * 1000; // Keep 10 minutes of data.\n\n    parse_ms_option!(\n        &args.general.retention,\n        config\n            .flags\n            .as_ref()\n            .and_then(|flags| flags.retention.as_ref()),\n        DEFAULT_RETENTION_MS,\n        \"retention\",\n        None,\n        None,\n    )\n}\n\nfn get_network_legend_position(\n    args: &BottomArgs, config: &Config,\n) -> OptionResult<Option<LegendPosition>> {\n    let result = if let Some(s) = &args.network.network_legend {\n        match s.to_ascii_lowercase().trim() {\n            \"none\" => None,\n            position => Some(parse_arg_value!(position.parse(), \"network_legend\")?),\n        }\n    } else if let Some(flags) = &config.flags {\n        if let Some(s) = &flags.network_legend {\n            match s.to_ascii_lowercase().trim() {\n                \"none\" => None,\n                position => Some(parse_config_value!(position.parse(), \"network_legend\")?),\n            }\n        } else {\n            Some(LegendPosition::default())\n        }\n    } else {\n        Some(LegendPosition::default())\n    };\n\n    Ok(result)\n}\n\nfn get_memory_legend_position(\n    args: &BottomArgs, config: &Config,\n) -> OptionResult<Option<LegendPosition>> {\n    let result = if let Some(s) = &args.memory.memory_legend {\n        match s.to_ascii_lowercase().trim() {\n            \"none\" => None,\n            position => Some(parse_arg_value!(position.parse(), \"memory_legend\")?),\n        }\n    } else if let Some(flags) = &config.flags {\n        if let Some(s) = &flags.memory_legend {\n            match s.to_ascii_lowercase().trim() {\n                \"none\" => None,\n                position => Some(parse_config_value!(position.parse(), \"memory_legend\")?),\n            }\n        } else {\n            Some(LegendPosition::default())\n        }\n    } else {\n        Some(LegendPosition::default())\n    };\n\n    Ok(result)\n}\n\n#[cfg(test)]\nmod test {\n    use clap::Parser;\n\n    use super::{Config, get_time_interval};\n    use crate::{\n        app::App,\n        args::BottomArgs,\n        options::{\n            config::flags::GeneralConfig, get_default_time_value, get_retention, get_update_rate,\n            try_parse_ms,\n        },\n    };\n\n    #[test]\n    fn verify_try_parse_ms() {\n        let a = \"100s\";\n        let b = \"100\";\n        let c = \"1 min\";\n        let d = \"1 hour 1 min\";\n\n        assert_eq!(try_parse_ms(a), Ok(100 * 1000));\n        assert_eq!(try_parse_ms(b), Ok(100));\n        assert_eq!(try_parse_ms(c), Ok(60 * 1000));\n        assert_eq!(try_parse_ms(d), Ok(3660 * 1000));\n\n        let a_bad = \"1 test\";\n        let b_bad = \"-100\";\n\n        assert!(try_parse_ms(a_bad).is_err());\n        assert!(try_parse_ms(b_bad).is_err());\n    }\n\n    #[test]\n    fn matches_human_times() {\n        let config = Config::default();\n\n        {\n            let delta_args = vec![\"btm\", \"--time_delta\", \"2 min\"];\n            let args = BottomArgs::parse_from(delta_args);\n\n            assert_eq!(\n                get_time_interval(&args, &config, 60 * 60 * 1000),\n                Ok(2 * 60 * 1000)\n            );\n        }\n\n        {\n            let default_time_args = vec![\"btm\", \"--default_time_value\", \"300s\"];\n            let args = BottomArgs::parse_from(default_time_args);\n\n            assert_eq!(\n                get_default_time_value(&args, &config, 60 * 60 * 1000),\n                Ok(5 * 60 * 1000)\n            );\n        }\n    }\n\n    #[test]\n    fn matches_number_times() {\n        let config = Config::default();\n\n        {\n            let delta_args = vec![\"btm\", \"--time_delta\", \"120000\"];\n            let args = BottomArgs::parse_from(delta_args);\n\n            assert_eq!(\n                get_time_interval(&args, &config, 60 * 60 * 1000),\n                Ok(2 * 60 * 1000)\n            );\n        }\n\n        {\n            let default_time_args = vec![\"btm\", \"--default_time_value\", \"300000\"];\n            let args = BottomArgs::parse_from(default_time_args);\n\n            assert_eq!(\n                get_default_time_value(&args, &config, 60 * 60 * 1000),\n                Ok(5 * 60 * 1000)\n            );\n        }\n    }\n\n    #[test]\n    fn config_human_times() {\n        let args = BottomArgs::parse_from([\"btm\"]);\n\n        let mut config = Config::default();\n        let flags = GeneralConfig {\n            time_delta: Some(\"2 min\".to_string().into()),\n            default_time_value: Some(\"300s\".to_string().into()),\n            rate: Some(\"1s\".to_string().into()),\n            retention: Some(\"10m\".to_string().into()),\n            ..Default::default()\n        };\n\n        config.flags = Some(flags);\n\n        assert_eq!(\n            get_time_interval(&args, &config, 60 * 60 * 1000),\n            Ok(2 * 60 * 1000)\n        );\n\n        assert_eq!(\n            get_default_time_value(&args, &config, 60 * 60 * 1000),\n            Ok(5 * 60 * 1000)\n        );\n\n        assert_eq!(get_update_rate(&args, &config), Ok(1000));\n\n        assert_eq!(get_retention(&args, &config), Ok(600000));\n    }\n\n    #[test]\n    fn config_number_times_as_string() {\n        let args = BottomArgs::parse_from([\"btm\"]);\n\n        let mut config = Config::default();\n        let flags = GeneralConfig {\n            time_delta: Some(\"120000\".to_string().into()),\n            default_time_value: Some(\"300000\".to_string().into()),\n            rate: Some(\"1000\".to_string().into()),\n            retention: Some(\"600000\".to_string().into()),\n            ..Default::default()\n        };\n\n        config.flags = Some(flags);\n\n        assert_eq!(\n            get_time_interval(&args, &config, 60 * 60 * 1000),\n            Ok(2 * 60 * 1000)\n        );\n\n        assert_eq!(\n            get_default_time_value(&args, &config, 60 * 60 * 1000),\n            Ok(5 * 60 * 1000)\n        );\n\n        assert_eq!(get_update_rate(&args, &config), Ok(1000));\n\n        assert_eq!(get_retention(&args, &config), Ok(600000));\n    }\n\n    #[test]\n    fn config_number_times_as_num() {\n        let args = BottomArgs::parse_from([\"btm\"]);\n\n        let mut config = Config::default();\n        let flags = GeneralConfig {\n            time_delta: Some(120000.into()),\n            default_time_value: Some(300000.into()),\n            rate: Some(1000.into()),\n            retention: Some(600000.into()),\n            ..Default::default()\n        };\n\n        config.flags = Some(flags);\n\n        assert_eq!(\n            get_time_interval(&args, &config, 60 * 60 * 1000),\n            Ok(2 * 60 * 1000)\n        );\n\n        assert_eq!(\n            get_default_time_value(&args, &config, 60 * 60 * 1000),\n            Ok(5 * 60 * 1000)\n        );\n\n        assert_eq!(get_update_rate(&args, &config), Ok(1000));\n\n        assert_eq!(get_retention(&args, &config), Ok(600000));\n    }\n\n    fn create_app(args: BottomArgs) -> App {\n        let config = Config::default();\n        super::init_app(args, config).unwrap().0\n    }\n\n    // TODO: There's probably a better way to create clap options AND unify together\n    // to avoid the possibility of typos/mixing up. Use proc macros to unify on\n    // one struct?\n    #[test]\n    fn verify_cli_options_build() {\n        let app = crate::args::build_cmd();\n\n        let default_app = create_app(BottomArgs::parse_from([\"btm\"]));\n\n        // Skip battery since it's tricky to test depending on the platform/features\n        // we're testing with.\n        let skip = [\"help\", \"version\", \"celsius\", \"battery\", \"generate_schema\"];\n\n        for arg in app.get_arguments().collect::<Vec<_>>() {\n            let arg_name = arg\n                .get_long_and_visible_aliases()\n                .unwrap()\n                .first()\n                .unwrap()\n                .to_owned();\n\n            if !arg.get_action().takes_values() && !skip.contains(&arg_name) {\n                let arg = format!(\"--{arg_name}\");\n\n                let arguments = vec![\"btm\", &arg];\n                let args = BottomArgs::parse_from(arguments);\n                let testing_app = create_app(args);\n\n                if (default_app.app_config_fields == testing_app.app_config_fields)\n                    && default_app.is_expanded == testing_app.is_expanded\n                    && default_app\n                        .states\n                        .proc_state\n                        .widget_states\n                        .iter()\n                        .zip(testing_app.states.proc_state.widget_states.iter())\n                        .all(|(a, b)| a.1.test_equality(b.1))\n                {\n                    panic!(\"failed on {arg_name}\");\n                }\n            }\n        }\n    }\n\n    /// This one has slightly more complex behaviour due to `dirs` not respecting XDG on macOS, so we manually\n    /// handle it. However, to ensure backwards-compatibility, we also have to do some special cases.\n    #[cfg(target_os = \"macos\")]\n    #[test]\n    fn test_get_config_path_macos() {\n        use std::path::PathBuf;\n\n        use super::{DEFAULT_CONFIG_FILE_LOCATION, get_config_path};\n\n        // Case three: no previous config, no XDG var.\n        // SAFETY: This is fine, this is just a test, and no other test affects env vars.\n        unsafe {\n            std::env::remove_var(\"XDG_CONFIG_HOME\");\n        }\n\n        let case_1 = dirs::config_dir()\n            .map(|mut path| {\n                path.push(DEFAULT_CONFIG_FILE_LOCATION);\n                path\n            })\n            .unwrap();\n\n        // Skip this test if the file already exists.\n        if !case_1.exists() {\n            assert_eq!(get_config_path(None), Some(case_1));\n        }\n\n        // Case two: no previous config, XDG var exists.\n        // SAFETY: This is fine, this is just a test, and no other test affects env vars.\n        unsafe {\n            std::env::set_var(\"XDG_CONFIG_HOME\", \"/tmp\");\n        }\n        let mut case_2 = PathBuf::new();\n        case_2.push(\"/tmp\");\n        case_2.push(DEFAULT_CONFIG_FILE_LOCATION);\n\n        // Skip this test if the file already exists.\n        if !case_2.exists() {\n            assert_eq!(get_config_path(None), Some(case_2));\n        }\n\n        // Case one: old non-XDG exists already, XDG var exists.\n        // let case_3 = case_1;\n        // assert_eq!(get_config_path(None), Some(case_1));\n    }\n}\n"
  },
  {
    "path": "src/utils/cancellation_token.rs",
    "content": "use std::{\n    sync::{Condvar, Mutex},\n    time::Duration,\n};\n\n/// A cancellation token.\npub(crate) struct CancellationToken {\n    // The \"check\" for the cancellation token. Setting this to true will mark the cancellation token as \"cancelled\".\n    mutex: Mutex<bool>,\n    cvar: Condvar,\n}\n\nimpl Default for CancellationToken {\n    fn default() -> Self {\n        Self {\n            mutex: Mutex::new(false),\n            cvar: Condvar::new(),\n        }\n    }\n}\n\nimpl CancellationToken {\n    /// Mark the [`CancellationToken`] as cancelled.\n    ///\n    /// This is idempotent, and once cancelled, will stay cancelled. Sending it\n    /// again will not do anything.\n    pub fn cancel(&self) {\n        let mut guard = self\n            .mutex\n            .lock()\n            .expect(\"cancellation token lock should not be poisoned\");\n\n        if !*guard {\n            *guard = true;\n            self.cvar.notify_all();\n        }\n    }\n\n    /// Try and check the [`CancellationToken`]'s status. Note that\n    /// this will not block.\n    pub fn try_check(&self) -> Option<bool> {\n        self.mutex.try_lock().ok().map(|guard| *guard)\n    }\n\n    /// Allows a thread to sleep while still being interruptible with by the token.\n    ///\n    /// Returns the condition state after either sleeping or being woken up.\n    pub fn sleep_with_cancellation(&self, duration: Duration) -> bool {\n        let guard = self\n            .mutex\n            .lock()\n            .expect(\"cancellation token lock should not be poisoned\");\n\n        let (result, _) = self\n            .cvar\n            .wait_timeout(guard, duration)\n            .expect(\"cancellation token lock should not be poisoned\");\n\n        *result\n    }\n}\n"
  },
  {
    "path": "src/utils/conversion.rs",
    "content": "//! This mainly concerns converting collected data into things that the canvas\n//! can actually handle.\n\nuse crate::utils::data_units::*;\n\n/// Returns the most appropriate binary prefix unit type (e.g. kibibyte) and\n/// denominator for the given amount of bytes.\n///\n/// The expected usage is to divide out the given value with the returned\n/// denominator in order to be able to use it with the returned binary unit\n/// (e.g. divide 3000 bytes by 1024 to have a value in KiB).\n#[inline]\npub(crate) fn get_binary_unit_and_denominator(bytes: u64) -> (&'static str, f64) {\n    match bytes {\n        b if b < KIBI_LIMIT => (\"B\", 1.0),\n        b if b < MEBI_LIMIT => (\"KiB\", KIBI_LIMIT_F64),\n        b if b < GIBI_LIMIT => (\"MiB\", MEBI_LIMIT_F64),\n        b if b < TEBI_LIMIT => (\"GiB\", GIBI_LIMIT_F64),\n        _ => (\"TiB\", TEBI_LIMIT_F64),\n    }\n}\n\n/// Returns a string given a value that is converted to the closest SI-variant,\n/// per second. If the value is greater than a giga-X, then it will return a\n/// decimal place.\n#[inline]\npub(crate) fn dec_bytes_per_second_string(value: u64) -> String {\n    let converted_values = get_decimal_bytes(value);\n    if value >= GIGA_LIMIT {\n        format!(\"{:.1}{}/s\", converted_values.0, converted_values.1)\n    } else {\n        format!(\"{:.0}{}/s\", converted_values.0, converted_values.1)\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn test_dec_bytes_per_second_string() {\n        assert_eq!(dec_bytes_per_second_string(0), \"0B/s\".to_string());\n        assert_eq!(dec_bytes_per_second_string(1), \"1B/s\".to_string());\n        assert_eq!(dec_bytes_per_second_string(900), \"900B/s\".to_string());\n        assert_eq!(dec_bytes_per_second_string(999), \"999B/s\".to_string());\n        assert_eq!(dec_bytes_per_second_string(KILO_LIMIT), \"1KB/s\".to_string());\n        assert_eq!(\n            dec_bytes_per_second_string(KILO_LIMIT + 1),\n            \"1KB/s\".to_string()\n        );\n        assert_eq!(dec_bytes_per_second_string(KIBI_LIMIT), \"1KB/s\".to_string());\n        assert_eq!(dec_bytes_per_second_string(MEGA_LIMIT), \"1MB/s\".to_string());\n        assert_eq!(\n            dec_bytes_per_second_string(GIGA_LIMIT),\n            \"1.0GB/s\".to_string()\n        );\n        assert_eq!(\n            dec_bytes_per_second_string(2 * GIGA_LIMIT),\n            \"2.0GB/s\".to_string()\n        );\n        assert_eq!(\n            dec_bytes_per_second_string((2.5 * GIGA_LIMIT as f64) as u64),\n            \"2.5GB/s\".to_string()\n        );\n        assert_eq!(\n            dec_bytes_per_second_string((10.34 * TERA_LIMIT as f64) as u64),\n            \"10.3TB/s\".to_string()\n        );\n        assert_eq!(\n            dec_bytes_per_second_string((10.36 * TERA_LIMIT as f64) as u64),\n            \"10.4TB/s\".to_string()\n        );\n    }\n}\n"
  },
  {
    "path": "src/utils/data_units.rs",
    "content": "#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]\npub enum DataUnit {\n    Byte,\n    #[default]\n    Bit,\n}\n\npub const KILO_LIMIT: u64 = 1000;\npub const MEGA_LIMIT: u64 = 1_000_000;\npub const GIGA_LIMIT: u64 = 1_000_000_000;\npub const TERA_LIMIT: u64 = 1_000_000_000_000;\npub const KIBI_LIMIT: u64 = 1024;\npub const MEBI_LIMIT: u64 = 1024 * 1024;\npub const GIBI_LIMIT: u64 = 1024 * 1024 * 1024;\npub const TEBI_LIMIT: u64 = 1024 * 1024 * 1024 * 1024;\n\npub const KILO_LIMIT_F64: f64 = 1000.0;\npub const MEGA_LIMIT_F64: f64 = 1_000_000.0;\npub const GIGA_LIMIT_F64: f64 = 1_000_000_000.0;\npub const TERA_LIMIT_F64: f64 = 1_000_000_000_000.0;\npub const KIBI_LIMIT_F64: f64 = 1024.0;\npub const MEBI_LIMIT_F64: f64 = 1024.0 * 1024.0;\npub const GIBI_LIMIT_F64: f64 = 1024.0 * 1024.0 * 1024.0;\npub const TEBI_LIMIT_F64: f64 = 1024.0 * 1024.0 * 1024.0 * 1024.0;\n\npub const LOG_MEGA_LIMIT: f64 = 6.0;\npub const LOG_GIGA_LIMIT: f64 = 9.0;\npub const LOG_TERA_LIMIT: f64 = 12.0;\npub const LOG_PETA_LIMIT: f64 = 15.0;\n\npub const LOG_MEBI_LIMIT: f64 = 20.0;\npub const LOG_GIBI_LIMIT: f64 = 30.0;\npub const LOG_TEBI_LIMIT: f64 = 40.0;\npub const LOG_PEBI_LIMIT: f64 = 50.0;\n\n/// Returns a tuple containing the value and the unit in bytes. In units of\n/// 1024. This only supports up to a tebi.  Note the \"single\" unit will have a\n/// space appended to match the others if `spacing` is true.\n#[inline]\npub fn get_binary_bytes(bytes: u64) -> (f64, &'static str) {\n    match bytes {\n        b if b < KIBI_LIMIT => (bytes as f64, \"B\"),\n        b if b < MEBI_LIMIT => (bytes as f64 / KIBI_LIMIT_F64, \"KiB\"),\n        b if b < GIBI_LIMIT => (bytes as f64 / MEBI_LIMIT_F64, \"MiB\"),\n        b if b < TEBI_LIMIT => (bytes as f64 / GIBI_LIMIT_F64, \"GiB\"),\n        _ => (bytes as f64 / TEBI_LIMIT_F64, \"TiB\"),\n    }\n}\n\n/// Returns a tuple containing the value and the unit in bytes. In units of\n/// 1000. This only supports up to a tera.  Note the \"single\" unit will have a\n/// space appended to match the others if `spacing` is true.\n#[inline]\npub fn get_decimal_bytes(bytes: u64) -> (f64, &'static str) {\n    match bytes {\n        b if b < KILO_LIMIT => (bytes as f64, \"B\"),\n        b if b < MEGA_LIMIT => (bytes as f64 / KILO_LIMIT_F64, \"KB\"),\n        b if b < GIGA_LIMIT => (bytes as f64 / MEGA_LIMIT_F64, \"MB\"),\n        b if b < TERA_LIMIT => (bytes as f64 / GIGA_LIMIT_F64, \"GB\"),\n        _ => (bytes as f64 / TERA_LIMIT_F64, \"TB\"),\n    }\n}\n\n/// Given a value in _bits_, turn a tuple containing the value and a unit.\n#[inline]\npub fn convert_bits(bits: u64, base_two: bool) -> (f64, &'static str) {\n    convert_bytes(bits / 8, base_two)\n}\n\n/// Given a value in _bytes_, turn a tuple containing the value and a unit.\n#[inline]\npub fn convert_bytes(bytes: u64, base_two: bool) -> (f64, &'static str) {\n    if base_two {\n        get_binary_bytes(bytes)\n    } else {\n        get_decimal_bytes(bytes)\n    }\n}\n\n/// Return a tuple containing the value and a unit string to be used as a prefix.\n#[inline]\npub fn get_unit_prefix(value: u64, base_two: bool) -> (f64, &'static str) {\n    let float_value = value as f64;\n\n    if base_two {\n        match value {\n            b if b < KIBI_LIMIT => (float_value, \"\"),\n            b if b < MEBI_LIMIT => (float_value / KIBI_LIMIT_F64, \"Ki\"),\n            b if b < GIBI_LIMIT => (float_value / MEBI_LIMIT_F64, \"Mi\"),\n            b if b < TEBI_LIMIT => (float_value / GIBI_LIMIT_F64, \"Gi\"),\n            _ => (float_value / TEBI_LIMIT_F64, \"Ti\"),\n        }\n    } else {\n        match value {\n            b if b < KILO_LIMIT => (float_value, \"\"),\n            b if b < MEGA_LIMIT => (float_value / KILO_LIMIT_F64, \"K\"),\n            b if b < GIGA_LIMIT => (float_value / MEGA_LIMIT_F64, \"M\"),\n            b if b < TERA_LIMIT => (float_value / GIGA_LIMIT_F64, \"G\"),\n            _ => (float_value / TERA_LIMIT_F64, \"T\"),\n        }\n    }\n}\n"
  },
  {
    "path": "src/utils/general.rs",
    "content": "use std::cmp::Ordering;\n\n#[inline]\npub(crate) const fn sort_partial_fn<T: PartialOrd>(is_descending: bool) -> fn(T, T) -> Ordering {\n    if is_descending {\n        partial_ordering_desc\n    } else {\n        partial_ordering\n    }\n}\n\n/// Returns an [`Ordering`] between two [`PartialOrd`]s.\n#[inline]\npub(crate) fn partial_ordering<T: PartialOrd>(a: T, b: T) -> Ordering {\n    a.partial_cmp(&b).unwrap_or(Ordering::Equal)\n}\n\n/// Returns a reversed [`Ordering`] between two [`PartialOrd`]s.\n///\n/// This is simply a wrapper function around [`partial_ordering`] that reverses\n/// the result.\n#[inline]\npub(crate) fn partial_ordering_desc<T: PartialOrd>(a: T, b: T) -> Ordering {\n    partial_ordering(a, b).reverse()\n}\n\n/// A trait for additional clamping functions on numeric types.\npub(crate) trait ClampExt {\n    /// Restrict a value by a lower bound. If the current value is _lower_ than\n    /// `lower_bound`, it will be set to `_lower_bound`.\n    #[cfg_attr(not(test), expect(dead_code))]\n    fn clamp_lower(&self, lower_bound: Self) -> Self;\n\n    /// Restrict a value by an upper bound. If the current value is _greater_\n    /// than `upper_bound`, it will be set to `upper_bound`.\n    fn clamp_upper(&self, upper_bound: Self) -> Self;\n}\n\nmacro_rules! clamp_num_impl {\n    ( $($NumType:ty),+ $(,)? ) => {\n        $(\n            impl ClampExt for $NumType {\n                fn clamp_lower(&self, lower_bound: Self) -> Self {\n                    if *self < lower_bound {\n                        lower_bound\n                    } else {\n                        *self\n                    }\n                }\n\n                fn clamp_upper(&self, upper_bound: Self) -> Self {\n                    if *self > upper_bound {\n                        upper_bound\n                    } else {\n                        *self\n                    }\n                }\n            }\n        )*\n    };\n}\n\nclamp_num_impl!(u8, u16, u32, u64, usize);\n\n/// Checked log2.\npub(crate) fn saturating_log2(value: f64) -> f64 {\n    if value > 0.0 { value.log2() } else { 0.0 }\n}\n\n/// Checked log10.\npub(crate) fn saturating_log10(value: f64) -> f64 {\n    if value > 0.0 { value.log10() } else { 0.0 }\n}\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn test_clamp_upper() {\n        let val: usize = 100;\n        assert_eq!(val.clamp_upper(150), 100);\n\n        let val: usize = 100;\n        assert_eq!(val.clamp_upper(100), 100);\n\n        let val: usize = 100;\n        assert_eq!(val.clamp_upper(50), 50);\n    }\n\n    #[test]\n    fn test_clamp_lower() {\n        let val: usize = 100;\n        assert_eq!(val.clamp_lower(150), 150);\n\n        let val: usize = 100;\n        assert_eq!(val.clamp_lower(100), 100);\n\n        let val: usize = 100;\n        assert_eq!(val.clamp_lower(50), 100);\n    }\n\n    #[test]\n    fn test_sort_partial_fn() {\n        let mut x = vec![9, 5, 20, 15, 10, 5];\n        let mut y = vec![1.0, 15.0, -1.0, -100.0, -100.1, 16.15, -100.0];\n\n        x.sort_by(|a, b| sort_partial_fn(false)(a, b));\n        assert_eq!(x, vec![5, 5, 9, 10, 15, 20]);\n\n        x.sort_by(|a, b| sort_partial_fn(true)(a, b));\n        assert_eq!(x, vec![20, 15, 10, 9, 5, 5]);\n\n        y.sort_by(|a, b| sort_partial_fn(false)(a, b));\n        assert_eq!(y, vec![-100.1, -100.0, -100.0, -1.0, 1.0, 15.0, 16.15]);\n\n        y.sort_by(|a, b| sort_partial_fn(true)(a, b));\n        assert_eq!(y, vec![16.15, 15.0, 1.0, -1.0, -100.0, -100.0, -100.1]);\n    }\n}\n"
  },
  {
    "path": "src/utils/logging.rs",
    "content": "#[cfg(feature = \"logging\")]\nuse std::sync::OnceLock;\n\n#[cfg(feature = \"logging\")]\npub static OFFSET: OnceLock<time::UtcOffset> = OnceLock::new();\n\n#[cfg(feature = \"logging\")]\npub fn init_logger(\n    min_level: log::LevelFilter, debug_file_name: Option<&std::ffi::OsStr>,\n) -> anyhow::Result<()> {\n    let dispatch = fern::Dispatch::new()\n        .format(|out, message, record| {\n            let offset = OFFSET.get_or_init(|| {\n                time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC)\n            });\n\n            let offset_time = {\n                let utc = time::OffsetDateTime::now_utc();\n                utc.checked_to_offset(*offset).unwrap_or(utc)\n            };\n\n            out.finish(format_args!(\n                \"{}[{}][{}] {}\",\n                offset_time\n                    .format(&time::macros::format_description!(\n                        // The weird \"[[[\" is because we need to escape a bracket (\"[[\") to show\n                        // one \"[\". See https://time-rs.github.io/book/api/format-description.html\n                        \"[[[year]-[month]-[day]][[[hour]:[minute]:[second][subsecond digits:9]]\"\n                    ))\n                    .expect(\"log formatting shouldn't fail\"),\n                record.target(),\n                record.level(),\n                message\n            ))\n        })\n        .level(min_level);\n\n    if let Some(debug_file_name) = debug_file_name {\n        dispatch.chain(fern::log_file(debug_file_name)?).apply()?;\n    } else {\n        dispatch.chain(std::io::stdout()).apply()?;\n    }\n\n    Ok(())\n}\n\n#[macro_export]\nmacro_rules! error {\n    ($($x:tt)*) => {\n        #[cfg(feature = \"logging\")]\n        {\n            log::error!($($x)*);\n        }\n    };\n}\n\n#[macro_export]\nmacro_rules! warn {\n    ($($x:tt)*) => {\n        #[cfg(feature = \"logging\")]\n        {\n            log::warn!($($x)*);\n        }\n    };\n}\n\n#[macro_export]\nmacro_rules! info {\n    ($($x:tt)*) => {\n        #[cfg(feature = \"logging\")]\n        {\n            log::info!($($x)*);\n        }\n    };\n}\n\n#[macro_export]\nmacro_rules! debug {\n    ($($x:tt)*) => {\n        #[cfg(feature = \"logging\")]\n        {\n            log::debug!($($x)*);\n        }\n    };\n}\n\n#[macro_export]\nmacro_rules! trace {\n    ($($x:tt)*) => {\n        #[cfg(feature = \"logging\")]\n        {\n            log::trace!($($x)*);\n        }\n    };\n}\n\n#[macro_export]\nmacro_rules! log {\n    ($($x:tt)*) => {\n        #[cfg(feature = \"logging\")]\n        {\n            log::log!(log::Level::Trace, $($x)*);\n        }\n    };\n    ($level:expr, $($x:tt)*) => {\n        #[cfg(feature = \"logging\")]\n        {\n            log::log!($level, $($x)*);\n        }\n    };\n}\n\n#[macro_export]\nmacro_rules! info_every_n_secs {\n    ($n:expr, $($x:tt)*) => {\n        #[cfg(feature = \"logging\")]\n        {\n            $crate::log_every_n_secs!(log::Level::Info, $n, $($x)*);\n        }\n    };\n}\n\n#[macro_export]\nmacro_rules! log_every_n_secs {\n    ($level:expr, $n:expr, $($x:tt)*) => {\n        #[cfg(feature = \"logging\")]\n        {\n            static LAST_LOG: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);\n            let since_last_log = LAST_LOG.load(std::sync::atomic::Ordering::Relaxed);\n            let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).expect(\"should be valid\").as_secs();\n\n            if now - since_last_log > $n {\n                LAST_LOG.store(now, std::sync::atomic::Ordering::Relaxed);\n                log::log!($level, $($x)*);\n            }\n        }\n    };\n}\n\n#[cfg(test)]\nmod test {\n    #[cfg(feature = \"logging\")]\n    /// We do this to ensure that the test logger is only initialized _once_ for\n    /// things like the default test runner that run tests in the same process.\n    ///\n    /// This doesn't do anything if you use something like nextest, which runs\n    /// a test-per-process, but that's fine.\n    fn init_test_logger() {\n        use std::sync::Once;\n\n        static INIT: Once = Once::new();\n\n        INIT.call_once(|| {\n            super::init_logger(log::LevelFilter::Trace, None)\n                .expect(\"initializing the logger should succeed\");\n        });\n    }\n\n    #[cfg(feature = \"logging\")]\n    #[test]\n    fn test_logging_macros() {\n        init_test_logger();\n\n        error!(\"This is an error.\");\n        warn!(\"This is a warning.\");\n        info!(\"This is an info.\");\n        debug!(\"This is a debug.\");\n        info!(\"This is a trace.\");\n    }\n\n    #[cfg(feature = \"logging\")]\n    #[test]\n    fn test_log_every_macros() {\n        init_test_logger();\n\n        info_every_n_secs!(10, \"This is an info every 10 seconds.\");\n    }\n}\n"
  },
  {
    "path": "src/utils/process_killer.rs",
    "content": "//! This file is meant to house (OS specific) implementations on how to kill\n//! processes.\n\nuse anyhow::bail;\n#[cfg(target_os = \"windows\")]\nuse windows::Win32::{\n    Foundation::{CloseHandle, HANDLE},\n    System::Threading::{\n        OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, TerminateProcess,\n    },\n};\n\nuse crate::collection::processes::Pid;\n\n/// Based from [this SO answer](https://stackoverflow.com/a/55231715).\n#[cfg(target_os = \"windows\")]\nstruct Process(HANDLE);\n\n#[cfg(target_os = \"windows\")]\nimpl Process {\n    fn open(pid: u32) -> anyhow::Result<Process> {\n        // SAFETY: Windows API call, tread carefully with the args.\n        match unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, false, pid) } {\n            Ok(process) => Ok(Process(process)),\n            Err(_) => bail!(\"process may have already been terminated.\"),\n        }\n    }\n\n    fn kill(self) -> anyhow::Result<()> {\n        // SAFETY: Windows API call, this is safe as we are passing in the handle.\n        let result = unsafe { TerminateProcess(self.0, 1) };\n        if result.is_err() {\n            bail!(\"process may have already been terminated.\");\n        }\n\n        Ok(())\n    }\n}\n\n#[cfg(target_os = \"windows\")]\nimpl Drop for Process {\n    fn drop(&mut self) {\n        // SAFETY: Windows API call, this is safe as we are passing in the handle.\n        unsafe {\n            let _ = CloseHandle(self.0);\n        }\n    }\n}\n\n/// Kills a process, given a PID, for windows.\n#[cfg(target_os = \"windows\")]\npub fn kill_process_given_pid(pid: Pid) -> anyhow::Result<()> {\n    let process = Process::open(pid as u32)?;\n    process.kill()?;\n\n    Ok(())\n}\n\n/// Kills a process, given a PID, for UNIX.\n#[cfg(unix)]\npub fn kill_process_given_pid(pid: Pid, signal: usize) -> anyhow::Result<()> {\n    // SAFETY: the signal should be valid, and we act properly on an error (exit code not 0).\n    let output = unsafe { libc::kill(pid, signal as i32) };\n\n    if output != 0 {\n        // We had an error...\n        let err_code = std::io::Error::last_os_error().raw_os_error();\n        let err = match err_code {\n            Some(libc::ESRCH) => \"the target process did not exist.\",\n            Some(libc::EPERM) => {\n                \"the calling process does not have the permissions to terminate the target process(es).\"\n            }\n            Some(libc::EINVAL) => \"an invalid signal was specified.\",\n            _ => \"Unknown error occurred.\",\n        };\n\n        if let Some(err_code) = err_code {\n            bail!(format!(\"Error code {err_code} - {err}\"))\n        } else {\n            bail!(format!(\"Error code unknown - {err}\"))\n        };\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "src/utils/strings.rs",
    "content": "use tui::text::Text;\nuse unicode_ellipsis::truncate_str;\n\n/// Truncates text if it is too long, and adds an ellipsis at the end if needed.\n///\n/// TODO: Maybe cache results from this function for some cases? e.g. columns\n#[inline]\npub fn truncate_to_text<'a, U: Into<usize>>(content: &str, width: U) -> Text<'a> {\n    Text::raw(truncate_str(content, width.into()).to_string())\n}\n\n/// Checks that the first string is equal to any of the other ones in a ASCII\n/// case-insensitive match.\n///\n/// The generated code is the same as writing:\n/// `to_ascii_lowercase(a) == to_ascii_lowercase(b) || to_ascii_lowercase(a) ==\n/// to_ascii_lowercase(c)`, but without allocating and copying temporaries.\n///\n/// # Examples\n///\n/// ```ignore\n/// assert!(multi_eq_ignore_ascii_case!(\"test\", \"test\"));\n/// assert!(multi_eq_ignore_ascii_case!(\"test\", \"a\" | \"b\" | \"test\"));\n/// assert!(!multi_eq_ignore_ascii_case!(\"test\", \"a\" | \"b\" | \"c\"));\n/// ```\n#[macro_export]\nmacro_rules! multi_eq_ignore_ascii_case {\n    ( $lhs:expr, $last:literal ) => {\n        $lhs.eq_ignore_ascii_case($last)\n    };\n    ( $lhs:expr, $head:literal | $($tail:tt)* ) => {\n        $lhs.eq_ignore_ascii_case($head) || multi_eq_ignore_ascii_case!($lhs, $($tail)*)\n    };\n}\n\n#[cfg(test)]\nmod tests {\n\n    #[test]\n    fn test_multi_eq_ignore_ascii_case() {\n        assert!(\n            multi_eq_ignore_ascii_case!(\"test\", \"test\"),\n            \"single comparison should succeed\"\n        );\n        assert!(\n            multi_eq_ignore_ascii_case!(\"test\", \"a\" | \"test\"),\n            \"double comparison should succeed\"\n        );\n        assert!(\n            multi_eq_ignore_ascii_case!(\"test\", \"a\" | \"b\" | \"test\"),\n            \"multi comparison should succeed\"\n        );\n\n        assert!(\n            !multi_eq_ignore_ascii_case!(\"test\", \"a\"),\n            \"single non-matching should fail\"\n        );\n        assert!(\n            !multi_eq_ignore_ascii_case!(\"test\", \"a\" | \"b\"),\n            \"double non-matching should fail\"\n        );\n        assert!(\n            !multi_eq_ignore_ascii_case!(\"test\", \"a\" | \"b\" | \"c\"),\n            \"multi non-matching should fail\"\n        );\n    }\n}\n"
  },
  {
    "path": "src/widgets/battery_info.rs",
    "content": "#[derive(Default)]\npub struct BatteryWidgetState {\n    pub currently_selected_battery_index: usize,\n    pub tab_click_locs: Option<Vec<((u16, u16), (u16, u16))>>,\n}\n"
  },
  {
    "path": "src/widgets/cpu_graph.rs",
    "content": "use std::{borrow::Cow, num::NonZeroU16, time::Instant};\n\nuse concat_string::concat_string;\nuse tui::widgets::Row;\n\nuse crate::{\n    app::AppConfigFields,\n    canvas::{\n        Painter,\n        components::data_table::{\n            Column, ColumnHeader, DataTable, DataTableColumn, DataTableProps, DataTableStyling,\n            DataToCell,\n        },\n    },\n    collection::cpu::{CpuData, CpuDataType},\n    options::config::{cpu::CpuDefault, style::Styles},\n};\n\npub enum CpuWidgetColumn {\n    Cpu,\n    Use,\n}\n\nimpl ColumnHeader for CpuWidgetColumn {\n    fn text(&self) -> Cow<'static, str> {\n        match self {\n            CpuWidgetColumn::Cpu => \"CPU\".into(),\n            CpuWidgetColumn::Use => \"Use\".into(),\n        }\n    }\n}\n\npub enum CpuWidgetTableData {\n    All,\n    Entry { data_type: CpuDataType, usage: f32 },\n}\n\nimpl CpuWidgetTableData {\n    pub fn from_cpu_data(data: &CpuData) -> CpuWidgetTableData {\n        CpuWidgetTableData::Entry {\n            data_type: data.data_type,\n            usage: data.usage,\n        }\n    }\n}\n\nimpl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {\n    fn to_cell_text(\n        &self, column: &CpuWidgetColumn, calculated_width: NonZeroU16,\n    ) -> Option<Cow<'static, str>> {\n        const CPU_TRUNCATE_BREAKPOINT: u16 = 5;\n\n        let calculated_width = calculated_width.get();\n\n        // This is a bit of a hack, but apparently we can avoid having to do any fancy\n        // checks of showing the \"All\" on a specific column if the other is\n        // hidden by just always showing it on the CPU (first) column - if there\n        // isn't room for it, it will just collapse down.\n        //\n        // This is the same for the use percentages - we just *always* show them, and\n        // *always* hide the CPU column if it is too small.\n        match &self {\n            CpuWidgetTableData::All => match column {\n                CpuWidgetColumn::Cpu => Some(\"All\".into()),\n                CpuWidgetColumn::Use => None,\n            },\n            CpuWidgetTableData::Entry {\n                data_type,\n                usage: last_entry,\n            } => {\n                if calculated_width == 0 {\n                    None\n                } else {\n                    match column {\n                        CpuWidgetColumn::Cpu => match data_type {\n                            CpuDataType::Avg => Some(\"AVG\".into()),\n                            CpuDataType::Cpu(index) => {\n                                let index_str = index.to_string();\n                                let text = if calculated_width < CPU_TRUNCATE_BREAKPOINT {\n                                    index_str.into()\n                                } else {\n                                    concat_string!(\"CPU\", index_str).into()\n                                };\n\n                                Some(text)\n                            }\n                        },\n                        CpuWidgetColumn::Use => Some(format!(\"{:.0}%\", last_entry.round()).into()),\n                    }\n                }\n            }\n        }\n    }\n\n    #[inline(always)]\n    fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {\n        let style = match self {\n            CpuWidgetTableData::All => painter.styles.all_cpu_colour,\n            CpuWidgetTableData::Entry {\n                data_type,\n                usage: _,\n            } => match data_type {\n                CpuDataType::Avg => painter.styles.avg_cpu_colour,\n                CpuDataType::Cpu(index) => {\n                    painter.styles.cpu_colour_styles[index % painter.styles.cpu_colour_styles.len()]\n                }\n            },\n        };\n\n        row.style(style)\n    }\n\n    fn column_widths<C: DataTableColumn<CpuWidgetColumn>>(\n        _data: &[Self], _columns: &[C],\n    ) -> Vec<u16>\n    where\n        Self: Sized,\n    {\n        vec![1, 3]\n    }\n}\n\npub struct CpuWidgetState {\n    pub current_display_time: u64,\n    pub is_legend_hidden: bool,\n    pub autohide_timer: Option<Instant>,\n    pub table: DataTable<CpuWidgetTableData, CpuWidgetColumn>,\n    pub force_update_data: bool,\n}\n\nimpl CpuWidgetState {\n    pub(crate) fn new(\n        config: &AppConfigFields, default_selection: CpuDefault, current_display_time: u64,\n        autohide_timer: Option<Instant>, colours: &Styles,\n    ) -> Self {\n        const COLUMNS: [Column<CpuWidgetColumn>; 2] = [\n            Column::soft(CpuWidgetColumn::Cpu, Some(0.5)),\n            Column::soft(CpuWidgetColumn::Use, Some(0.5)),\n        ];\n\n        let props = DataTableProps {\n            title: None,\n            table_gap: config.table_gap,\n            left_to_right: false,\n            is_basic: false,\n            show_table_scroll_position: false, // TODO: Should this be possible?\n            show_current_entry_when_unfocused: true,\n        };\n\n        let styling = DataTableStyling::from_palette(colours);\n        let mut table = DataTable::new(COLUMNS, props, styling);\n        match default_selection {\n            CpuDefault::All => {}\n            CpuDefault::Average if !config.show_average_cpu => {}\n            CpuDefault::Average => {\n                table = table.first_draw_index(1);\n            }\n        }\n\n        CpuWidgetState {\n            current_display_time,\n            is_legend_hidden: false,\n            autohide_timer,\n            table,\n            force_update_data: false,\n        }\n    }\n\n    /// Forces an update of the data stored.\n    #[inline]\n    pub fn force_data_update(&mut self) {\n        self.force_update_data = true;\n    }\n\n    pub fn set_legend_data(&mut self, data: &[CpuData]) {\n        self.table.set_data(\n            std::iter::once(CpuWidgetTableData::All)\n                .chain(data.iter().map(CpuWidgetTableData::from_cpu_data))\n                .collect(),\n        );\n        self.force_update_data = false;\n    }\n}\n"
  },
  {
    "path": "src/widgets/disk_table.rs",
    "content": "use std::{borrow::Cow, cmp::max, num::NonZeroU16};\n\nuse serde::Deserialize;\n\nuse crate::{\n    app::{AppConfigFields, data::StoredData},\n    canvas::components::data_table::{\n        ColumnHeader, DataTableColumn, DataTableProps, DataTableStyling, DataToCell, SortColumn,\n        SortDataTable, SortDataTableProps, SortOrder, SortsRow,\n    },\n    options::config::style::Styles,\n    utils::{\n        conversion::dec_bytes_per_second_string, data_units::get_decimal_bytes,\n        general::sort_partial_fn,\n    },\n};\n\n#[derive(Clone, Debug)]\npub struct DiskWidgetData {\n    pub name: String,\n    pub mount_point: String,\n    pub free_bytes: Option<u64>,\n    pub used_bytes: Option<u64>,\n    pub total_bytes: Option<u64>,\n    pub summed_total_bytes: Option<u64>,\n    pub io_read_rate_bytes: Option<u64>,\n    pub io_write_rate_bytes: Option<u64>,\n}\n\nimpl DiskWidgetData {\n    fn total_space(&self) -> Cow<'static, str> {\n        if let Some(total_bytes) = self.total_bytes {\n            let converted_total_space = get_decimal_bytes(total_bytes);\n            format!(\"{:.0}{}\", converted_total_space.0, converted_total_space.1).into()\n        } else {\n            \"N/A\".into()\n        }\n    }\n\n    fn free_space(&self) -> Cow<'static, str> {\n        if let Some(free_bytes) = self.free_bytes {\n            let converted_free_space = get_decimal_bytes(free_bytes);\n            format!(\"{:.0}{}\", converted_free_space.0, converted_free_space.1).into()\n        } else {\n            \"N/A\".into()\n        }\n    }\n\n    fn used_space(&self) -> Cow<'static, str> {\n        if let Some(used_bytes) = self.used_bytes {\n            let converted_free_space = get_decimal_bytes(used_bytes);\n            format!(\"{:.0}{}\", converted_free_space.0, converted_free_space.1).into()\n        } else {\n            \"N/A\".into()\n        }\n    }\n\n    fn free_percent(&self) -> Option<f64> {\n        if let (Some(free_bytes), Some(summed_total_bytes)) =\n            (self.free_bytes, self.summed_total_bytes)\n        {\n            if summed_total_bytes > 0 {\n                Some(free_bytes as f64 / summed_total_bytes as f64 * 100_f64)\n            } else {\n                None\n            }\n        } else {\n            None\n        }\n    }\n\n    fn used_percent(&self) -> Option<f64> {\n        if let (Some(used_bytes), Some(summed_total_bytes)) =\n            (self.used_bytes, self.summed_total_bytes)\n        {\n            if summed_total_bytes > 0 {\n                Some(used_bytes as f64 / summed_total_bytes as f64 * 100_f64)\n            } else {\n                None\n            }\n        } else {\n            None\n        }\n    }\n\n    fn io_read(&self) -> Cow<'static, str> {\n        self.io_read_rate_bytes.map_or(\"N/A\".into(), |r_rate| {\n            dec_bytes_per_second_string(r_rate).into()\n        })\n    }\n\n    fn io_write(&self) -> Cow<'static, str> {\n        self.io_write_rate_bytes.map_or(\"N/A\".into(), |w_rate| {\n            dec_bytes_per_second_string(w_rate).into()\n        })\n    }\n}\n\n#[derive(Debug, Clone)]\n#[cfg_attr(\n    feature = \"generate_schema\",\n    derive(schemars::JsonSchema, strum::VariantArray)\n)]\n#[cfg_attr(test, derive(PartialEq, Eq))]\npub enum DiskColumn {\n    Disk,\n    Mount,\n    Used,\n    Free,\n    Total,\n    UsedPercent,\n    FreePercent,\n    IoRead,\n    IoWrite,\n}\n\nimpl<'de> Deserialize<'de> for DiskColumn {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: serde::Deserializer<'de>,\n    {\n        let value = String::deserialize(deserializer)?.to_lowercase();\n        match value.as_str() {\n            \"disk\" => Ok(DiskColumn::Disk),\n            \"mount\" => Ok(DiskColumn::Mount),\n            \"used\" => Ok(DiskColumn::Used),\n            \"free\" => Ok(DiskColumn::Free),\n            \"total\" => Ok(DiskColumn::Total),\n            \"usedpercent\" | \"used%\" => Ok(DiskColumn::UsedPercent),\n            \"freepercent\" | \"free%\" => Ok(DiskColumn::FreePercent),\n            \"r/s\" => Ok(DiskColumn::IoRead),\n            \"w/s\" => Ok(DiskColumn::IoWrite),\n            _ => Err(serde::de::Error::custom(\n                \"doesn't match any disk column name\",\n            )),\n        }\n    }\n}\n\nimpl DiskColumn {\n    /// An ugly hack to generate the JSON schema.\n    #[cfg(feature = \"generate_schema\")]\n    pub fn get_schema_names(&self) -> &[&'static str] {\n        match self {\n            DiskColumn::Disk => &[\"Disk\"],\n            DiskColumn::Mount => &[\"Mount\"],\n            DiskColumn::Used => &[\"Used\"],\n            DiskColumn::Free => &[\"Free\"],\n            DiskColumn::Total => &[\"Total\"],\n            DiskColumn::UsedPercent => &[\"Used%\"],\n            DiskColumn::FreePercent => &[\"Free%\"],\n            DiskColumn::IoRead => &[\"R/s\", \"Read\", \"Rps\"],\n            DiskColumn::IoWrite => &[\"W/s\", \"Write\", \"Wps\"],\n        }\n    }\n}\n\nimpl ColumnHeader for DiskColumn {\n    fn text(&self) -> Cow<'static, str> {\n        match self {\n            DiskColumn::Disk => \"Disk(d)\",\n            DiskColumn::Mount => \"Mount(m)\",\n            DiskColumn::Used => \"Used(u)\",\n            DiskColumn::Free => \"Free(n)\",\n            DiskColumn::Total => \"Total(t)\",\n            DiskColumn::UsedPercent => \"Used%(p)\",\n            DiskColumn::FreePercent => \"Free%\",\n            DiskColumn::IoRead => \"R/s(r)\",\n            DiskColumn::IoWrite => \"W/s(w)\",\n        }\n        .into()\n    }\n}\n\nimpl DataToCell<DiskColumn> for DiskWidgetData {\n    // FIXME: (points_rework_v1) Can we change the return type to 'a instead of 'static?\n    fn to_cell_text(\n        &self, column: &DiskColumn, _calculated_width: NonZeroU16,\n    ) -> Option<Cow<'static, str>> {\n        fn percent_string(value: Option<f64>) -> Cow<'static, str> {\n            match value {\n                Some(val) => format!(\"{val:.1}%\").into(),\n                None => \"N/A\".into(),\n            }\n        }\n\n        let text = match column {\n            DiskColumn::Disk => self.name.clone().into(),\n            DiskColumn::Mount => self.mount_point.clone().into(),\n            DiskColumn::Used => self.used_space(),\n            DiskColumn::Free => self.free_space(),\n            DiskColumn::UsedPercent => percent_string(self.used_percent()),\n            DiskColumn::FreePercent => percent_string(self.free_percent()),\n            DiskColumn::Total => self.total_space(),\n            DiskColumn::IoRead => self.io_read(),\n            DiskColumn::IoWrite => self.io_write(),\n        };\n\n        Some(text)\n    }\n\n    fn column_widths<C: DataTableColumn<DiskColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>\n    where\n        Self: Sized,\n    {\n        let mut widths = vec![0; 7];\n\n        data.iter().for_each(|row| {\n            widths[0] = max(widths[0], row.name.len() as u16);\n            widths[1] = max(widths[1], row.mount_point.len() as u16);\n        });\n\n        widths\n    }\n}\n\npub struct DiskTableWidget {\n    pub table: SortDataTable<DiskWidgetData, DiskColumn>,\n    pub force_update_data: bool,\n}\n\nimpl SortsRow for DiskColumn {\n    type DataType = DiskWidgetData;\n\n    fn sort_data(&self, data: &mut [Self::DataType], descending: bool) {\n        match self {\n            DiskColumn::Disk => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(&a.name, &b.name));\n            }\n            DiskColumn::Mount => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(&a.mount_point, &b.mount_point));\n            }\n            DiskColumn::Used => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(&a.used_bytes, &b.used_bytes));\n            }\n            DiskColumn::UsedPercent => {\n                data.sort_by(|a, b| {\n                    sort_partial_fn(descending)(&a.used_percent(), &b.used_percent())\n                });\n            }\n            DiskColumn::Free => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(&a.free_bytes, &b.free_bytes));\n            }\n            DiskColumn::FreePercent => {\n                data.sort_by(|a, b| {\n                    sort_partial_fn(descending)(&a.free_percent(), &b.free_percent())\n                });\n            }\n            DiskColumn::Total => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(&a.total_bytes, &b.total_bytes));\n            }\n            DiskColumn::IoRead => {\n                data.sort_by(|a, b| {\n                    sort_partial_fn(descending)(&a.io_read_rate_bytes, &b.io_read_rate_bytes)\n                });\n            }\n            DiskColumn::IoWrite => {\n                data.sort_by(|a, b| {\n                    sort_partial_fn(descending)(&a.io_write_rate_bytes, &b.io_write_rate_bytes)\n                });\n            }\n        }\n    }\n}\n\nconst fn create_column(column_type: &DiskColumn) -> SortColumn<DiskColumn> {\n    match column_type {\n        DiskColumn::Disk => SortColumn::soft(DiskColumn::Disk, Some(0.2)),\n        DiskColumn::Mount => SortColumn::soft(DiskColumn::Mount, Some(0.2)),\n        DiskColumn::Used => SortColumn::hard(DiskColumn::Used, 8).default_descending(),\n        DiskColumn::Free => SortColumn::hard(DiskColumn::Free, 8).default_descending(),\n        DiskColumn::Total => SortColumn::hard(DiskColumn::Total, 9).default_descending(),\n        DiskColumn::UsedPercent => {\n            SortColumn::hard(DiskColumn::UsedPercent, 9).default_descending()\n        }\n        DiskColumn::FreePercent => {\n            SortColumn::hard(DiskColumn::FreePercent, 9).default_descending()\n        }\n        DiskColumn::IoRead => SortColumn::hard(DiskColumn::IoRead, 10).default_descending(),\n        DiskColumn::IoWrite => SortColumn::hard(DiskColumn::IoWrite, 11).default_descending(),\n    }\n}\n\nconst fn default_disk_columns() -> [SortColumn<DiskColumn>; 8] {\n    [\n        create_column(&DiskColumn::Disk),\n        create_column(&DiskColumn::Mount),\n        create_column(&DiskColumn::Used),\n        create_column(&DiskColumn::Free),\n        create_column(&DiskColumn::Total),\n        create_column(&DiskColumn::UsedPercent),\n        create_column(&DiskColumn::IoRead),\n        create_column(&DiskColumn::IoWrite),\n    ]\n}\n\nimpl DiskTableWidget {\n    pub fn new(config: &AppConfigFields, palette: &Styles, columns: Option<&[DiskColumn]>) -> Self {\n        let props = SortDataTableProps {\n            inner: DataTableProps {\n                title: Some(\" Disks \".into()),\n                table_gap: config.table_gap,\n                left_to_right: true,\n                is_basic: config.use_basic_mode,\n                show_table_scroll_position: config.show_table_scroll_position,\n                show_current_entry_when_unfocused: false,\n            },\n            sort_index: 0,\n            order: SortOrder::Ascending,\n        };\n\n        let styling = DataTableStyling::from_palette(palette);\n\n        match columns {\n            Some(columns) => {\n                let columns = columns.iter().map(create_column).collect::<Vec<_>>();\n                Self {\n                    table: SortDataTable::new_sortable(columns, props, styling),\n                    force_update_data: false,\n                }\n            }\n            None => Self {\n                table: SortDataTable::new_sortable(default_disk_columns(), props, styling),\n                force_update_data: false,\n            },\n        }\n    }\n\n    /// Forces an update of the data stored.\n    #[inline]\n    pub fn force_data_update(&mut self) {\n        self.force_update_data = true;\n    }\n\n    /// Update the current table data.\n    pub fn set_table_data(&mut self, data: &StoredData) {\n        let mut data = data.disk_harvest.clone();\n\n        if let Some(column) = self.table.columns.get(self.table.sort_index()) {\n            column.sort_by(&mut data, self.table.order());\n        }\n        self.table.set_data(data);\n        self.force_update_data = false;\n    }\n\n    pub fn set_index(&mut self, index: usize) {\n        self.table.set_sort_index(index);\n        self.force_data_update();\n    }\n}\n"
  },
  {
    "path": "src/widgets/mem_graph.rs",
    "content": "use std::time::Instant;\n\npub struct MemWidgetState {\n    pub current_display_time: u64,\n    pub autohide_timer: Option<Instant>,\n}\n\nimpl MemWidgetState {\n    pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {\n        MemWidgetState {\n            current_display_time,\n            autohide_timer,\n        }\n    }\n}\n"
  },
  {
    "path": "src/widgets/mod.rs",
    "content": "pub mod battery_info;\npub mod cpu_graph;\npub mod disk_table;\npub mod mem_graph;\npub mod network_graph;\npub mod process_table;\npub mod temperature_table;\n\npub use battery_info::*;\npub use cpu_graph::*;\npub use disk_table::*;\npub use mem_graph::*;\npub use network_graph::*;\npub use process_table::*;\npub use temperature_table::*;\n"
  },
  {
    "path": "src/widgets/network_graph.rs",
    "content": "use std::time::Instant;\n\npub struct NetWidgetState {\n    pub current_display_time: u64,\n    pub autohide_timer: Option<Instant>,\n    pub height_cache: Option<NetWidgetHeightCache>,\n}\n\npub struct NetWidgetHeightCache {\n    pub best_point: (Instant, f64),\n    pub right_edge: Instant,\n    pub period: u64,\n}\n\nimpl NetWidgetState {\n    pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {\n        NetWidgetState {\n            current_display_time,\n            autohide_timer,\n            height_cache: None,\n        }\n    }\n}\n"
  },
  {
    "path": "src/widgets/process_table/process_columns.rs",
    "content": "use std::{borrow::Cow, cmp::Reverse};\n\nuse serde::Deserialize;\n\nuse super::{ProcWidgetColumn, ProcWidgetData};\nuse crate::{\n    canvas::components::data_table::{ColumnHeader, SortsRow},\n    utils::general::sort_partial_fn,\n};\n\n/// A column in the process widget.\n#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]\n#[cfg_attr(\n    feature = \"generate_schema\",\n    derive(schemars::JsonSchema, strum::VariantArray)\n)]\npub enum ProcColumn {\n    CpuPercent,\n    MemValue,\n    MemPercent,\n    VirtualMem,\n    Pid,\n    Count,\n    Name,\n    Command,\n    ReadPerSecond,\n    WritePerSecond,\n    TotalRead,\n    TotalWrite,\n    State,\n    User,\n    Time,\n    #[cfg(unix)]\n    Nice,\n    Priority,\n    #[cfg(feature = \"gpu\")]\n    GpuMemValue,\n    #[cfg(feature = \"gpu\")]\n    GpuMemPercent,\n    #[cfg(feature = \"gpu\")]\n    GpuUtilPercent,\n}\n\nimpl ProcColumn {\n    /// An ugly hack to generate the JSON schema.\n    #[cfg(feature = \"generate_schema\")]\n    pub fn get_schema_names(&self) -> &[&'static str] {\n        match self {\n            ProcColumn::Pid => &[\"PID\"],\n            ProcColumn::Count => &[\"Count\"],\n            ProcColumn::Name => &[\"Name\"],\n            ProcColumn::Command => &[\"Command\"],\n            ProcColumn::CpuPercent => &[\"CPU%\"],\n            // TODO: Change this\n            ProcColumn::MemValue | ProcColumn::MemPercent => &[\"Mem\", \"Mem%\", \"Memory\", \"Memory%\"],\n            ProcColumn::VirtualMem => &[\"Virt\", \"Virtual\", \"VirtMem\", \"Virtual Memory\"],\n            ProcColumn::ReadPerSecond => &[\"R/s\", \"Read\", \"Rps\"],\n            ProcColumn::WritePerSecond => &[\"W/s\", \"Write\", \"Wps\"],\n            ProcColumn::TotalRead => &[\"T.Read\", \"TRead\", \"Total Read\"],\n            ProcColumn::TotalWrite => &[\"T.Write\", \"TWrite\", \"Total Write\"],\n            ProcColumn::State => &[\"State\"],\n            ProcColumn::User => &[\"User\"],\n            ProcColumn::Time => &[\"Time\"],\n            #[cfg(feature = \"gpu\")]\n            // TODO: Change this\n            ProcColumn::GpuMemValue | ProcColumn::GpuMemPercent => &[\"GMem\", \"GMem%\"],\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuUtilPercent => &[\"GPU%\"],\n            #[cfg(unix)]\n            ProcColumn::Nice => &[\"Nice\"],\n            ProcColumn::Priority => &[\"Priority\"],\n        }\n    }\n}\n\nimpl ColumnHeader for ProcColumn {\n    fn text(&self) -> Cow<'static, str> {\n        match self {\n            ProcColumn::CpuPercent => \"CPU%\",\n            ProcColumn::MemValue => \"Mem\",\n            ProcColumn::MemPercent => \"Mem%\",\n            ProcColumn::VirtualMem => \"Virt\",\n            ProcColumn::Pid => \"PID\",\n            ProcColumn::Count => \"Count\",\n            ProcColumn::Name => \"Name\",\n            ProcColumn::Command => \"Command\",\n            ProcColumn::ReadPerSecond => \"R/s\",\n            ProcColumn::WritePerSecond => \"W/s\",\n            ProcColumn::TotalRead => \"T.Read\",\n            ProcColumn::TotalWrite => \"T.Write\",\n            ProcColumn::State => \"State\",\n            ProcColumn::User => \"User\",\n            ProcColumn::Time => \"Time\",\n            #[cfg(unix)]\n            ProcColumn::Nice => \"Nice\",\n            ProcColumn::Priority => \"Priority\",\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuMemValue => \"GMem\",\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuMemPercent => \"GMem%\",\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuUtilPercent => \"GPU%\",\n        }\n        .into()\n    }\n\n    fn header(&self) -> Cow<'static, str> {\n        match self {\n            ProcColumn::CpuPercent => \"CPU%(c)\".into(),\n            ProcColumn::MemValue => \"Mem(m)\".into(),\n            ProcColumn::MemPercent => \"Mem%(m)\".into(),\n            ProcColumn::Pid => \"PID(p)\".into(),\n            ProcColumn::Name => \"Name(n)\".into(),\n            ProcColumn::Command => \"Command(n)\".into(),\n            #[cfg(unix)]\n            ProcColumn::Nice => \"Nice\".into(),\n            ProcColumn::Priority => \"Priority\".into(),\n            _ => self.text(),\n        }\n    }\n}\n\nimpl SortsRow for ProcColumn {\n    type DataType = ProcWidgetData;\n\n    fn sort_data(&self, data: &mut [ProcWidgetData], descending: bool) {\n        match self {\n            ProcColumn::CpuPercent => {\n                data.sort_by(|a, b| {\n                    sort_partial_fn(descending)(a.cpu_usage_percent, b.cpu_usage_percent)\n                });\n            }\n            ProcColumn::MemValue | ProcColumn::MemPercent => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(&a.mem_usage, &b.mem_usage));\n            }\n            ProcColumn::VirtualMem => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(&a.virtual_mem, &b.virtual_mem));\n            }\n            ProcColumn::Pid => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(a.pid, b.pid));\n            }\n            ProcColumn::Count => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(a.num_similar, b.num_similar));\n            }\n            ProcColumn::Name | ProcColumn::Command => {\n                if descending {\n                    data.sort_by_cached_key(|pd| Reverse(pd.id.to_lowercase()));\n                } else {\n                    data.sort_by_cached_key(|pd| pd.id.to_lowercase());\n                }\n            }\n            ProcColumn::ReadPerSecond => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(a.rps, b.rps));\n            }\n            ProcColumn::WritePerSecond => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(a.wps, b.wps));\n            }\n            ProcColumn::TotalRead => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(a.total_read, b.total_read));\n            }\n            ProcColumn::TotalWrite => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(a.total_write, b.total_write));\n            }\n            ProcColumn::State => {\n                if descending {\n                    data.sort_by_cached_key(|pd| Reverse(pd.process_state));\n                } else {\n                    data.sort_by_cached_key(|pd| pd.process_state);\n                }\n            }\n            ProcColumn::User => {\n                // FIXME: Is there a better way here to keep the to_lowercase? Usually it shouldn't matter but...\n                if descending {\n                    data.sort_by_cached_key(|pd| Reverse(pd.user.clone()));\n                } else {\n                    data.sort_by_cached_key(|pd| pd.user.clone());\n                }\n            }\n            ProcColumn::Time => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(a.time, b.time));\n            }\n            ProcColumn::Priority => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(a.priority, b.priority));\n            }\n            #[cfg(unix)]\n            ProcColumn::Nice => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(a.nice, b.nice));\n            }\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuMemValue | ProcColumn::GpuMemPercent => {\n                data.sort_by(|a, b| {\n                    sort_partial_fn(descending)(&a.gpu_mem_usage, &b.gpu_mem_usage)\n                });\n            }\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuUtilPercent => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(a.gpu_usage, b.gpu_usage));\n            }\n        }\n    }\n}\n\nimpl<'de> Deserialize<'de> for ProcColumn {\n    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>\n    where\n        D: serde::Deserializer<'de>,\n    {\n        let value = String::deserialize(deserializer)?.to_lowercase();\n        match value.as_str() {\n            \"cpu%\" => Ok(ProcColumn::CpuPercent),\n            \"mem\" | \"mem%\" => Ok(ProcColumn::MemPercent),\n            \"virt\" | \"virtual\" | \"virtmem\" | \"virtual memory\" => Ok(ProcColumn::VirtualMem),\n            \"pid\" => Ok(ProcColumn::Pid),\n            \"count\" => Ok(ProcColumn::Count),\n            \"name\" => Ok(ProcColumn::Name),\n            \"command\" => Ok(ProcColumn::Command),\n            \"read\" | \"r/s\" | \"rps\" => Ok(ProcColumn::ReadPerSecond),\n            \"write\" | \"w/s\" | \"wps\" => Ok(ProcColumn::WritePerSecond),\n            \"tread\" | \"t.read\" => Ok(ProcColumn::TotalRead),\n            \"twrite\" | \"t.write\" => Ok(ProcColumn::TotalWrite),\n            \"state\" => Ok(ProcColumn::State),\n            \"user\" => Ok(ProcColumn::User),\n            \"time\" => Ok(ProcColumn::Time),\n            #[cfg(unix)]\n            \"nice\" => Ok(ProcColumn::Nice),\n            \"priority\" => Ok(ProcColumn::Priority),\n            #[cfg(feature = \"gpu\")]\n            \"gmem\" | \"gmem%\" => Ok(ProcColumn::GpuMemPercent),\n            #[cfg(feature = \"gpu\")]\n            \"gpu%\" => Ok(ProcColumn::GpuUtilPercent),\n            _ => Err(serde::de::Error::custom(\n                \"doesn't match any process column name\",\n            )),\n        }\n    }\n}\n\nimpl From<&ProcColumn> for ProcWidgetColumn {\n    fn from(value: &ProcColumn) -> Self {\n        match value {\n            ProcColumn::Pid | ProcColumn::Count => ProcWidgetColumn::PidOrCount,\n            ProcColumn::Name | ProcColumn::Command => ProcWidgetColumn::ProcNameOrCommand,\n            ProcColumn::CpuPercent => ProcWidgetColumn::Cpu,\n            ProcColumn::MemPercent | ProcColumn::MemValue => ProcWidgetColumn::Mem,\n            ProcColumn::VirtualMem => ProcWidgetColumn::VirtualMem,\n            ProcColumn::ReadPerSecond => ProcWidgetColumn::ReadPerSecond,\n            ProcColumn::WritePerSecond => ProcWidgetColumn::WritePerSecond,\n            ProcColumn::TotalRead => ProcWidgetColumn::TotalRead,\n            ProcColumn::TotalWrite => ProcWidgetColumn::TotalWrite,\n            ProcColumn::State => ProcWidgetColumn::State,\n            ProcColumn::User => ProcWidgetColumn::User,\n            ProcColumn::Time => ProcWidgetColumn::Time,\n            ProcColumn::Priority => ProcWidgetColumn::Priority,\n            #[cfg(unix)]\n            ProcColumn::Nice => ProcWidgetColumn::Nice,\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuMemPercent | ProcColumn::GpuMemValue => ProcWidgetColumn::GpuMem,\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuUtilPercent => ProcWidgetColumn::GpuUtil,\n        }\n    }\n}\n"
  },
  {
    "path": "src/widgets/process_table/process_data.rs",
    "content": "use std::{\n    borrow::Cow,\n    cmp::{Ordering, max},\n    fmt::Display,\n    num::NonZeroU16,\n    sync::Arc,\n    time::Duration,\n};\n\nuse concat_string::concat_string;\nuse tui::widgets::Row;\n\nuse super::process_columns::ProcColumn;\nuse crate::{\n    canvas::{\n        Painter,\n        components::data_table::{DataTableColumn, DataToCell},\n    },\n    collection::processes::{Pid, ProcessHarvest},\n    dec_bytes_per_second_string,\n    utils::data_units::{GIBI_LIMIT, GIGA_LIMIT, get_binary_bytes, get_decimal_bytes},\n};\n\n#[derive(Clone, Debug)]\nenum IdType {\n    Name(String),\n    Command(String),\n}\n\n#[derive(Clone, Debug)]\npub struct Id {\n    id_type: IdType,\n    prefix: Option<String>,\n}\n\nimpl From<&'static str> for Id {\n    fn from(s: &'static str) -> Self {\n        Id {\n            id_type: IdType::Name(s.to_string()),\n            prefix: None,\n        }\n    }\n}\n\nimpl Id {\n    /// Returns the ID as a lowercase [`String`], with no prefix. This is\n    /// primarily useful for cases like sorting where we treat everything as\n    /// the same case (e.g. `Discord` comes before `dkms`).\n    pub fn to_lowercase(&self) -> String {\n        match &self.id_type {\n            IdType::Name(name) => name.to_lowercase(),\n            IdType::Command(cmd) => cmd.to_lowercase(),\n        }\n    }\n\n    /// Return the ID as a borrowed [`str`] with no prefix.\n    pub fn as_str(&self) -> &str {\n        match &self.id_type {\n            IdType::Name(name) => name.as_str(),\n            IdType::Command(cmd) => cmd.as_str(),\n        }\n    }\n\n    /// Returns the ID as a [`String`] with prefix.\n    pub fn to_prefixed_string(&self) -> String {\n        if let Some(prefix) = &self.prefix {\n            concat_string!(\n                prefix,\n                match &self.id_type {\n                    IdType::Name(name) => name,\n                    IdType::Command(cmd) => cmd,\n                }\n            )\n        } else {\n            match &self.id_type {\n                IdType::Name(name) => name.to_string(),\n                IdType::Command(cmd) => cmd.to_string(),\n            }\n        }\n    }\n}\n\nimpl Display for Id {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(self.as_str())\n    }\n}\n\n#[derive(PartialEq, Clone, Debug)]\npub enum MemUsage {\n    Percent(f32),\n    Bytes(u64),\n}\n\nimpl PartialOrd for MemUsage {\n    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {\n        match (self, other) {\n            (MemUsage::Percent(a), MemUsage::Percent(b)) => a.partial_cmp(b),\n            (MemUsage::Bytes(a), MemUsage::Bytes(b)) => a.partial_cmp(b),\n            _ => unreachable!(),\n        }\n    }\n}\n\nimpl Display for MemUsage {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            MemUsage::Percent(percent) => f.write_fmt(format_args!(\"{percent:.1}%\")),\n            MemUsage::Bytes(bytes) => f.write_str(&binary_byte_string(*bytes)),\n        }\n    }\n}\n\ntrait DurationExt {\n    fn num_days(&self) -> u64;\n    fn num_hours(&self) -> u64;\n    fn num_minutes(&self) -> u64;\n}\n\nconst SECS_PER_DAY: u64 = SECS_PER_HOUR * 24;\nconst SECS_PER_HOUR: u64 = SECS_PER_MINUTE * 60;\nconst SECS_PER_MINUTE: u64 = 60;\n\nimpl DurationExt for Duration {\n    /// Number of full days in this duration.\n    #[inline]\n    fn num_days(&self) -> u64 {\n        self.as_secs() / SECS_PER_DAY\n    }\n\n    /// Number of full hours in this duration.\n    #[inline]\n    fn num_hours(&self) -> u64 {\n        self.as_secs() / SECS_PER_HOUR\n    }\n\n    /// Number of full minutes in this duration.\n    #[inline]\n    fn num_minutes(&self) -> u64 {\n        self.as_secs() / SECS_PER_MINUTE\n    }\n}\n\nfn format_time(dur: Duration) -> String {\n    if dur.num_days() > 0 {\n        format!(\n            \"{}d {}h {}m\",\n            dur.num_days(),\n            dur.num_hours() % 24,\n            dur.num_minutes() % 60\n        )\n    } else if dur.num_hours() > 0 {\n        format!(\n            \"{}h {}m {}s\",\n            dur.num_hours(),\n            dur.num_minutes() % 60,\n            dur.as_secs() % 60\n        )\n    } else if dur.num_minutes() > 0 {\n        format!(\n            \"{}m {}.{:02}s\",\n            dur.num_minutes(),\n            dur.as_secs() % 60,\n            dur.as_millis() % 1000 / 10\n        )\n    } else {\n        format!(\"{}.{:03}s\", dur.as_secs(), dur.as_millis() % 1000)\n    }\n}\n\n/// Returns a string given a value that is converted to the closest binary\n/// variant. If the value is greater than a gibibyte, then it will return a\n/// decimal place.\n#[inline]\nfn binary_byte_string(value: u64) -> String {\n    let converted_values = get_binary_bytes(value);\n    if value >= GIBI_LIMIT {\n        format!(\"{:.1}{}\", converted_values.0, converted_values.1)\n    } else {\n        format!(\"{:.0}{}\", converted_values.0, converted_values.1)\n    }\n}\n\n/// Returns a string given a value that is converted to the closest SI-variant.\n/// If the value is greater than a giga-X, then it will return a decimal place.\nfn dec_bytes_string(value: u64) -> String {\n    let converted_values = get_decimal_bytes(value);\n    if value >= GIGA_LIMIT {\n        format!(\"{:.1}{}\", converted_values.0, converted_values.1)\n    } else {\n        format!(\"{:.0}{}\", converted_values.0, converted_values.1)\n    }\n}\n\n#[derive(Clone)]\npub struct ProcWidgetData {\n    pub pid: Pid,\n    #[allow(dead_code)]\n    pub ppid: Option<Pid>,\n    pub id: Id,\n    pub cpu_usage_percent: f32,\n    pub mem_usage: MemUsage,\n    pub virtual_mem: u64,\n    pub rps: u64,\n    pub wps: u64,\n    pub total_read: u64,\n    pub total_write: u64,\n    pub process_state: &'static str,\n    pub process_char: char,\n    pub user: Option<Arc<str>>,\n    pub num_similar: u64,\n    pub disabled: bool,\n    pub time: Duration,\n    #[cfg(feature = \"gpu\")]\n    pub gpu_mem_usage: MemUsage,\n    #[cfg(feature = \"gpu\")]\n    pub gpu_usage: u32,\n    /// The process \"type\". Used to color things.\n    #[cfg(target_os = \"linux\")]\n    pub process_type: crate::collection::processes::ProcessType,\n    #[cfg(unix)]\n    pub nice: i32,\n    pub priority: i32,\n}\n\nimpl ProcWidgetData {\n    pub fn from_data(process: &ProcessHarvest, is_command: bool, is_mem_percent: bool) -> Self {\n        let id = Id {\n            id_type: if is_command {\n                IdType::Command(process.command.clone())\n            } else {\n                IdType::Name(process.name.clone())\n            },\n            prefix: None,\n        };\n\n        let mem_usage = if is_mem_percent {\n            MemUsage::Percent(process.mem_usage_percent)\n        } else {\n            MemUsage::Bytes(process.mem_usage)\n        };\n\n        Self {\n            pid: process.pid,\n            ppid: process.parent_pid,\n            id,\n            cpu_usage_percent: process.cpu_usage_percent,\n            mem_usage,\n            virtual_mem: process.virtual_mem,\n            rps: process.read_per_sec,\n            wps: process.write_per_sec,\n            total_read: process.total_read,\n            total_write: process.total_write,\n            process_state: process.process_state.0,\n            process_char: process.process_state.1,\n            user: process.user.clone(),\n            num_similar: 1,\n            disabled: false,\n            time: process.time,\n            #[cfg(feature = \"gpu\")]\n            gpu_mem_usage: if is_mem_percent {\n                MemUsage::Percent(process.gpu_mem_percent)\n            } else {\n                MemUsage::Bytes(process.gpu_mem)\n            },\n            #[cfg(feature = \"gpu\")]\n            gpu_usage: process.gpu_util,\n            #[cfg(target_os = \"linux\")]\n            process_type: process.process_type,\n            #[cfg(unix)]\n            nice: process.nice,\n            priority: process.priority,\n        }\n    }\n\n    pub fn disabled(mut self, disabled: bool) -> Self {\n        self.disabled = disabled;\n        self\n    }\n\n    pub fn prefix(mut self, prefix: Option<String>) -> Self {\n        self.id.prefix = prefix;\n        self\n    }\n\n    pub fn add(&mut self, other: &Self) {\n        self.cpu_usage_percent += other.cpu_usage_percent;\n        self.mem_usage = match (&self.mem_usage, &other.mem_usage) {\n            (MemUsage::Percent(a), MemUsage::Percent(b)) => MemUsage::Percent(a + b),\n            (MemUsage::Bytes(a), MemUsage::Bytes(b)) => MemUsage::Bytes(a + b),\n            (MemUsage::Percent(_), MemUsage::Bytes(_))\n            | (MemUsage::Bytes(_), MemUsage::Percent(_)) => {\n                unreachable!(\"trying to add together two different memory usage types!\")\n            }\n        };\n        self.rps += other.rps;\n        self.wps += other.wps;\n        self.total_read += other.total_read;\n        self.total_write += other.total_write;\n        self.time = self.time.max(other.time);\n        #[cfg(feature = \"gpu\")]\n        {\n            self.gpu_mem_usage = match (&self.gpu_mem_usage, &other.gpu_mem_usage) {\n                (MemUsage::Percent(a), MemUsage::Percent(b)) => MemUsage::Percent(a + b),\n                (MemUsage::Bytes(a), MemUsage::Bytes(b)) => MemUsage::Bytes(a + b),\n                (MemUsage::Percent(_), MemUsage::Bytes(_))\n                | (MemUsage::Bytes(_), MemUsage::Percent(_)) => {\n                    unreachable!(\"trying to add together two different memory usage types!\")\n                }\n            };\n            self.gpu_usage += other.gpu_usage;\n        }\n    }\n\n    fn to_string(&self, column: &ProcColumn) -> String {\n        match column {\n            &ProcColumn::Priority => self.priority.to_string(),\n            #[cfg(unix)]\n            ProcColumn::Nice => self.nice.to_string(),\n            ProcColumn::CpuPercent => format!(\"{:.1}%\", self.cpu_usage_percent),\n            ProcColumn::MemValue | ProcColumn::MemPercent => self.mem_usage.to_string(),\n            ProcColumn::VirtualMem => binary_byte_string(self.virtual_mem),\n            ProcColumn::Pid => self.pid.to_string(),\n            ProcColumn::Count => self.num_similar.to_string(),\n            ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string(),\n            ProcColumn::ReadPerSecond => dec_bytes_per_second_string(self.rps),\n            ProcColumn::WritePerSecond => dec_bytes_per_second_string(self.wps),\n            ProcColumn::TotalRead => dec_bytes_string(self.total_read),\n            ProcColumn::TotalWrite => dec_bytes_string(self.total_write),\n            ProcColumn::State => self.process_char.to_string(),\n            ProcColumn::User => self\n                .user\n                .as_ref()\n                .map(|user| user.to_string())\n                .unwrap_or_else(|| \"N/A\".to_string()),\n            ProcColumn::Time => format_time(self.time),\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuMemValue | ProcColumn::GpuMemPercent => self.gpu_mem_usage.to_string(),\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuUtilPercent => format!(\"{:.1}%\", self.gpu_usage),\n        }\n    }\n}\n\nimpl DataToCell<ProcColumn> for ProcWidgetData {\n    fn to_cell_text(\n        &self, column: &ProcColumn, calculated_width: NonZeroU16,\n    ) -> Option<Cow<'static, str>> {\n        // TODO: Optimize the string allocations here...\n        // TODO: Also maybe just pull in the to_string call but add a variable for the\n        // differences.\n        Some(match column {\n            #[cfg(unix)]\n            ProcColumn::Nice => self.nice.to_string().into(),\n            &ProcColumn::Priority => self.priority.to_string().into(),\n            ProcColumn::CpuPercent => format!(\"{:.1}%\", self.cpu_usage_percent).into(),\n            ProcColumn::MemValue | ProcColumn::MemPercent => self.mem_usage.to_string().into(),\n            ProcColumn::VirtualMem => binary_byte_string(self.virtual_mem).into(),\n            ProcColumn::Pid => self.pid.to_string().into(),\n            ProcColumn::Count => self.num_similar.to_string().into(),\n            ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string().into(),\n            ProcColumn::ReadPerSecond => dec_bytes_per_second_string(self.rps).into(),\n            ProcColumn::WritePerSecond => dec_bytes_per_second_string(self.wps).into(),\n            ProcColumn::TotalRead => dec_bytes_string(self.total_read).into(),\n            ProcColumn::TotalWrite => dec_bytes_string(self.total_write).into(),\n            ProcColumn::State => {\n                if calculated_width.get() < 8 {\n                    self.process_char.to_string().into()\n                } else {\n                    self.process_state.into()\n                }\n            }\n            ProcColumn::User => self\n                .user\n                .as_ref()\n                .map(|user| user.to_string().into())\n                .unwrap_or_else(|| \"N/A\".into()),\n            ProcColumn::Time => format_time(self.time).into(),\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuMemValue | ProcColumn::GpuMemPercent => {\n                self.gpu_mem_usage.to_string().into()\n            }\n            #[cfg(feature = \"gpu\")]\n            ProcColumn::GpuUtilPercent => format!(\"{:.1}%\", self.gpu_usage).into(),\n        })\n    }\n\n    #[cfg(target_os = \"linux\")]\n    #[inline(always)]\n    fn style_cell(&self, column: &ProcColumn, painter: &Painter) -> Option<tui::style::Style> {\n        match column {\n            ProcColumn::Name | ProcColumn::Command if self.process_type.is_thread() => {\n                Some(painter.styles.thread_text_style)\n            }\n            _ => None,\n        }\n    }\n\n    #[inline(always)]\n    fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {\n        if self.disabled {\n            row.style(painter.styles.disabled_text_style)\n        } else {\n            row\n        }\n    }\n\n    fn column_widths<C: DataTableColumn<ProcColumn>>(data: &[Self], columns: &[C]) -> Vec<u16>\n    where\n        Self: Sized,\n    {\n        let mut widths = vec![0; columns.len()];\n\n        for d in data {\n            for (w, c) in widths.iter_mut().zip(columns) {\n                *w = max(*w, d.to_string(c.inner()).len() as u16);\n            }\n        }\n\n        widths\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use std::time::Duration;\n\n    use super::*;\n    use crate::utils::data_units::*;\n\n    #[test]\n    fn test_format_time() {\n        const ONE_DAY: u64 = 24 * 60 * 60;\n\n        assert_eq!(format_time(Duration::from_millis(500)), \"0.500s\");\n        assert_eq!(format_time(Duration::from_millis(900)), \"0.900s\");\n        assert_eq!(format_time(Duration::from_secs(1)), \"1.000s\");\n        assert_eq!(format_time(Duration::from_secs(10)), \"10.000s\");\n        assert_eq!(format_time(Duration::from_secs(60)), \"1m 0.00s\");\n        assert_eq!(format_time(Duration::from_secs(61)), \"1m 1.00s\");\n        assert_eq!(format_time(Duration::from_secs(600)), \"10m 0.00s\");\n        assert_eq!(format_time(Duration::from_secs(601)), \"10m 1.00s\");\n        assert_eq!(format_time(Duration::from_secs(3600)), \"1h 0m 0s\");\n        assert_eq!(format_time(Duration::from_secs(3601)), \"1h 0m 1s\");\n        assert_eq!(format_time(Duration::from_secs(3660)), \"1h 1m 0s\");\n        assert_eq!(format_time(Duration::from_secs(3661)), \"1h 1m 1s\");\n        assert_eq!(format_time(Duration::from_secs(ONE_DAY - 1)), \"23h 59m 59s\");\n        assert_eq!(format_time(Duration::from_secs(ONE_DAY)), \"1d 0h 0m\");\n        assert_eq!(format_time(Duration::from_secs(ONE_DAY + 1)), \"1d 0h 0m\");\n        assert_eq!(format_time(Duration::from_secs(ONE_DAY + 60)), \"1d 0h 1m\");\n        assert_eq!(\n            format_time(Duration::from_secs(ONE_DAY + 3600 - 1)),\n            \"1d 0h 59m\"\n        );\n        assert_eq!(format_time(Duration::from_secs(ONE_DAY + 3600)), \"1d 1h 0m\");\n        assert_eq!(\n            format_time(Duration::from_secs(ONE_DAY * 365 - 1)),\n            \"364d 23h 59m\"\n        );\n    }\n\n    #[test]\n    fn test_binary_byte_string() {\n        assert_eq!(binary_byte_string(0), \"0B\".to_string());\n        assert_eq!(binary_byte_string(1), \"1B\".to_string());\n        assert_eq!(binary_byte_string(1000), \"1000B\".to_string());\n        assert_eq!(binary_byte_string(1023), \"1023B\".to_string());\n        assert_eq!(binary_byte_string(KIBI_LIMIT), \"1KiB\".to_string());\n        assert_eq!(binary_byte_string(KIBI_LIMIT + 1), \"1KiB\".to_string());\n        assert_eq!(binary_byte_string(MEBI_LIMIT), \"1MiB\".to_string());\n        assert_eq!(binary_byte_string(GIBI_LIMIT), \"1.0GiB\".to_string());\n        assert_eq!(binary_byte_string(2 * GIBI_LIMIT), \"2.0GiB\".to_string());\n        assert_eq!(\n            binary_byte_string((2.5 * GIBI_LIMIT as f64) as u64),\n            \"2.5GiB\".to_string()\n        );\n        assert_eq!(\n            binary_byte_string((10.34 * TEBI_LIMIT as f64) as u64),\n            \"10.3TiB\".to_string()\n        );\n        assert_eq!(\n            binary_byte_string((10.36 * TEBI_LIMIT as f64) as u64),\n            \"10.4TiB\".to_string()\n        );\n    }\n}\n"
  },
  {
    "path": "src/widgets/process_table/query/and.rs",
    "content": "use std::collections::VecDeque;\n\nuse crate::{\n    collection::processes::ProcessHarvest,\n    widgets::query::{\n        COMPARISON_LIST, Or, Prefix, QueryOptions, QueryProcessor, QueryResult, error::QueryError,\n    },\n};\n\n/// A node where both the left hand side or the right hand side are considered.\n/// Note that the right hand side is optional, as that's how I implemented it a long time ago.\n#[derive(Debug)]\npub(super) struct And {\n    pub(super) lhs: Prefix,\n    // TODO: Maybe don't need to box rhs?\n    pub(super) rhs: Option<Box<Prefix>>,\n}\n\nimpl And {\n    pub(super) fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {\n        if let Some(rhs) = &self.rhs {\n            self.lhs.check(process, is_using_command) && rhs.check(process, is_using_command)\n        } else {\n            self.lhs.check(process, is_using_command)\n        }\n    }\n}\n\nimpl QueryProcessor for And {\n    fn process(query: &mut VecDeque<String>, options: &QueryOptions) -> QueryResult<Self>\n    where\n        Self: Sized,\n    {\n        const AND_LIST: [&str; 2] = [\"and\", \"&&\"];\n\n        let mut lhs = Prefix::process(query, options)?;\n        let mut rhs: Option<Box<Prefix>> = None;\n\n        while let Some(queue_top) = query.front() {\n            let current_lowercase = queue_top.to_lowercase();\n            if AND_LIST.contains(&current_lowercase.as_str()) {\n                query.pop_front();\n\n                rhs = Some(Box::new(Prefix::process(query, options)?));\n\n                if let Some(next_queue_top) = query.front() {\n                    if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) {\n                        // Must merge LHS and RHS\n                        lhs = Prefix::Or(Box::new(Or {\n                            lhs: And { lhs, rhs },\n                            rhs: None,\n                        }));\n                        rhs = None;\n                    } else {\n                        break;\n                    }\n                } else {\n                    break;\n                }\n            } else if COMPARISON_LIST.contains(&current_lowercase.as_str()) {\n                return Err(QueryError::new(\"Comparison not valid here\"));\n            } else {\n                break;\n            }\n        }\n\n        Ok(And { lhs, rhs })\n    }\n}\n"
  },
  {
    "path": "src/widgets/process_table/query/attribute.rs",
    "content": "//! Code related to attributes, which should be \"searchable\" leaf nodes.\n\nuse regex::Regex;\n\nuse crate::{\n    collection::processes::ProcessHarvest,\n    widgets::query::{\n        NumericalQuery, PrefixType, QueryOptions, TimeQuery,\n        error::{QueryError, QueryResult},\n        new_regex,\n    },\n};\n\n/// An attribute (leaf node) for a process.\n#[derive(Debug)]\npub(super) enum ProcessAttribute {\n    /// This is a bit of a hack to allow for \"empty\" attributes. We can fix it properly,\n    /// but it would potentially require handling \"empty\" queries better. Currently, we just\n    /// treat it as a leaf node that always succeeds on matches.\n    Empty,\n    Pid(Regex),\n    CpuPercentage(NumericalQuery),\n    MemBytes(NumericalQuery),\n    MemPercentage(NumericalQuery),\n    ReadPerSecond(NumericalQuery),\n    WritePerSecond(NumericalQuery),\n    TotalRead(NumericalQuery),\n    TotalWrite(NumericalQuery),\n    /// Note this is an \"untagged\" attribute (e.g. \"btm\", \"firefox\").\n    Name(Regex),\n    State(Regex),\n    User(Regex),\n    Time(TimeQuery),\n    #[cfg(unix)]\n    Nice(NumericalQuery),\n    Priority(NumericalQuery),\n    #[cfg(feature = \"gpu\")]\n    GpuPercentage(NumericalQuery),\n    #[cfg(feature = \"gpu\")]\n    GpuMemoryPercentage(NumericalQuery),\n    #[cfg(feature = \"gpu\")]\n    GpuMemoryBytes(NumericalQuery),\n}\n\nimpl ProcessAttribute {\n    pub(super) fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {\n        match self {\n            ProcessAttribute::Empty => true,\n            ProcessAttribute::Pid(re) => re.is_match(process.pid.to_string().as_str()),\n            ProcessAttribute::CpuPercentage(cmp) => cmp.check(process.cpu_usage_percent),\n            ProcessAttribute::MemBytes(cmp) => cmp.check(process.mem_usage as f64),\n            ProcessAttribute::MemPercentage(cmp) => cmp.check(process.mem_usage_percent),\n            ProcessAttribute::ReadPerSecond(cmp) => cmp.check(process.read_per_sec as f64),\n            ProcessAttribute::WritePerSecond(cmp) => cmp.check(process.write_per_sec as f64),\n            ProcessAttribute::TotalRead(cmp) => cmp.check(process.total_read as f64),\n            ProcessAttribute::TotalWrite(cmp) => cmp.check(process.total_write as f64),\n            ProcessAttribute::Name(re) => re.is_match(if is_using_command {\n                process.command.as_str()\n            } else {\n                process.name.as_str()\n            }),\n            ProcessAttribute::State(re) => re.is_match(process.process_state.0),\n            ProcessAttribute::User(re) => match process.user.as_ref() {\n                Some(user) => re.is_match(user),\n                None => re.is_match(\"N/A\"),\n            },\n            ProcessAttribute::Time(time) => time.check(process.time),\n            // TODO: It's a bit silly for some of these, like nice/priority, where it's casted to an f64.\n            #[cfg(unix)]\n            ProcessAttribute::Nice(cmp) => cmp.check(process.nice as f64),\n            ProcessAttribute::Priority(cmp) => cmp.check(process.priority as f64),\n            #[cfg(feature = \"gpu\")]\n            ProcessAttribute::GpuPercentage(cmp) => cmp.check(process.gpu_util as f64),\n            #[cfg(feature = \"gpu\")]\n            ProcessAttribute::GpuMemoryPercentage(cmp) => cmp.check(process.gpu_mem_percent as f64),\n            #[cfg(feature = \"gpu\")]\n            ProcessAttribute::GpuMemoryBytes(cmp) => cmp.check(process.gpu_mem as f64),\n        }\n    }\n}\n\n/// Given a string prefix type, obtain the appropriate [`ProcessAttribute`].\npub(super) fn new_string_attribute(\n    prefix_type: PrefixType, base: &str, regex_options: &QueryOptions,\n) -> QueryResult<ProcessAttribute> {\n    match prefix_type {\n        PrefixType::Pid | PrefixType::Name | PrefixType::State | PrefixType::User => {\n            let re = new_regex(base, regex_options)?;\n\n            match prefix_type {\n                PrefixType::Pid => Ok(ProcessAttribute::Pid(re)),\n                PrefixType::Name => Ok(ProcessAttribute::Name(re)),\n                PrefixType::State => Ok(ProcessAttribute::State(re)),\n                PrefixType::User => Ok(ProcessAttribute::User(re)),\n                _ => unreachable!(),\n            }\n        }\n        _ => Err(QueryError::new(format!(\n            \"process attribute type {prefix_type:?} is not a supported string attribute\"\n        ))),\n    }\n}\n\n/// Given a time prefix type, obtain the appropriate [`ProcessAttribute`].\npub(super) fn new_time_attribute(\n    prefix_type: PrefixType, query: TimeQuery,\n) -> QueryResult<ProcessAttribute> {\n    match prefix_type {\n        PrefixType::Time => Ok(ProcessAttribute::Time(query)),\n        _ => Err(QueryError::new(format!(\n            \"process attribute type {prefix_type:?} is not a supported time attribute\"\n        ))),\n    }\n}\n\n/// Given a numerical prefix type, obtain the appropriate [`ProcessAttribute`].\npub(super) fn new_numerical_attribute(\n    prefix_type: PrefixType, query: NumericalQuery,\n) -> QueryResult<ProcessAttribute> {\n    match prefix_type {\n        PrefixType::CpuPercentage => Ok(ProcessAttribute::CpuPercentage(query)),\n        PrefixType::MemBytes => Ok(ProcessAttribute::MemBytes(query)),\n        PrefixType::MemPercentage => Ok(ProcessAttribute::MemPercentage(query)),\n        PrefixType::ReadPerSecond => Ok(ProcessAttribute::ReadPerSecond(query)),\n        PrefixType::WritePerSecond => Ok(ProcessAttribute::WritePerSecond(query)),\n        PrefixType::TotalRead => Ok(ProcessAttribute::TotalRead(query)),\n        PrefixType::TotalWrite => Ok(ProcessAttribute::TotalWrite(query)),\n        #[cfg(unix)]\n        PrefixType::Nice => Ok(ProcessAttribute::Nice(query)),\n        PrefixType::Priority => Ok(ProcessAttribute::Priority(query)),\n        #[cfg(feature = \"gpu\")]\n        PrefixType::GpuPercentage => Ok(ProcessAttribute::GpuPercentage(query)),\n        #[cfg(feature = \"gpu\")]\n        PrefixType::GpuMemoryBytes => Ok(ProcessAttribute::GpuMemoryBytes(query)),\n        #[cfg(feature = \"gpu\")]\n        PrefixType::GpuMemoryPercentage => Ok(ProcessAttribute::GpuMemoryPercentage(query)),\n        _ => Err(QueryError::new(format!(\n            \"process attribute type {prefix_type:?} is not a supported numerical attribute\"\n        ))),\n    }\n}\n"
  },
  {
    "path": "src/widgets/process_table/query/error.rs",
    "content": "use std::{\n    borrow::Cow,\n    fmt::{Display, Formatter},\n};\n\n#[derive(Debug)]\npub(crate) struct QueryError {\n    reason: Cow<'static, str>,\n}\n\nimpl QueryError {\n    #[inline]\n    pub(crate) fn new<I: Into<Cow<'static, str>>>(reason: I) -> Self {\n        Self {\n            reason: reason.into(),\n        }\n    }\n\n    #[inline]\n    pub(super) fn missing_value() -> Self {\n        Self {\n            reason: \"Missing value\".into(),\n        }\n    }\n}\n\nimpl Display for QueryError {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.reason)\n    }\n}\n\nimpl From<regex::Error> for QueryError {\n    fn from(err: regex::Error) -> Self {\n        Self::new(err.to_string())\n    }\n}\n\npub(super) type QueryResult<T> = Result<T, QueryError>;\n"
  },
  {
    "path": "src/widgets/process_table/query/or.rs",
    "content": "use std::collections::VecDeque;\n\nuse crate::{\n    collection::processes::ProcessHarvest,\n    widgets::query::{\n        And, COMPARISON_LIST, Prefix, QueryOptions, QueryProcessor, QueryResult, error::QueryError,\n    },\n};\n\n/// A node where either the left-hand side or the right-hand side are considered.\n/// Note that the right-hand side is optional, as that's how I implemented it a long time ago.\n#[derive(Debug)]\npub(super) struct Or {\n    pub(super) lhs: And,\n    // TODO: Maybe don't need to box rhs?\n    pub(super) rhs: Option<Box<And>>,\n}\n\nimpl Or {\n    pub(super) fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {\n        if let Some(rhs) = &self.rhs {\n            self.lhs.check(process, is_using_command) || rhs.check(process, is_using_command)\n        } else {\n            self.lhs.check(process, is_using_command)\n        }\n    }\n}\n\nimpl QueryProcessor for Or {\n    fn process(query: &mut VecDeque<String>, options: &QueryOptions) -> QueryResult<Self>\n    where\n        Self: Sized,\n    {\n        const OR_LIST: [&str; 2] = [\"or\", \"||\"];\n\n        let mut lhs = And::process(query, options)?;\n        let mut rhs: Option<Box<And>> = None;\n\n        while let Some(queue_top) = query.front() {\n            let current_lowercase = queue_top.to_lowercase();\n            if OR_LIST.contains(&current_lowercase.as_str()) {\n                query.pop_front();\n                rhs = Some(Box::new(And::process(query, options)?));\n\n                if let Some(queue_next) = query.front() {\n                    if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {\n                        // Must merge LHS and RHS\n                        lhs = And {\n                            lhs: Prefix::Or(Box::new(Or { lhs, rhs })),\n                            rhs: None,\n                        };\n                        rhs = None;\n                    }\n                } else {\n                    break;\n                }\n            } else if COMPARISON_LIST.contains(&current_lowercase.as_str()) {\n                return Err(QueryError::new(\"Comparison not valid here\"));\n            } else {\n                break;\n            }\n        }\n\n        Ok(Or { lhs, rhs })\n    }\n}\n"
  },
  {
    "path": "src/widgets/process_table/query/prefix.rs",
    "content": "use std::{collections::VecDeque, fmt::Debug};\n\nuse humantime::parse_duration;\n\nuse crate::{\n    collection::processes::ProcessHarvest,\n    utils::data_units::*,\n    widgets::query::{\n        And, NumericalQuery, Or, PrefixType, ProcessAttribute, QueryComparison, QueryOptions,\n        QueryProcessor, QueryResult, TimeQuery,\n        attribute::{new_numerical_attribute, new_string_attribute, new_time_attribute},\n        error::QueryError,\n    },\n};\n\n#[inline]\nfn process_prefix_units(query: &mut VecDeque<String>, value: &mut f64) {\n    // If no unit, assume base.\n    //\n    // Furthermore, base must be PEEKED at initially, and will\n    // require (likely) prefix_type specific checks\n    // Lastly, if it *is* a unit, remember to POP!\n    if let Some(potential_unit) = query.front() {\n        if potential_unit.eq_ignore_ascii_case(\"tb\") {\n            *value *= TERA_LIMIT_F64;\n            query.pop_front();\n        } else if potential_unit.eq_ignore_ascii_case(\"tib\") {\n            *value *= TEBI_LIMIT_F64;\n            query.pop_front();\n        } else if potential_unit.eq_ignore_ascii_case(\"gb\") {\n            *value *= GIGA_LIMIT_F64;\n            query.pop_front();\n        } else if potential_unit.eq_ignore_ascii_case(\"gib\") {\n            *value *= GIBI_LIMIT_F64;\n            query.pop_front();\n        } else if potential_unit.eq_ignore_ascii_case(\"mb\") {\n            *value *= MEGA_LIMIT_F64;\n            query.pop_front();\n        } else if potential_unit.eq_ignore_ascii_case(\"mib\") {\n            *value *= MEBI_LIMIT_F64;\n            query.pop_front();\n        } else if potential_unit.eq_ignore_ascii_case(\"kb\") {\n            *value *= KILO_LIMIT_F64;\n            query.pop_front();\n        } else if potential_unit.eq_ignore_ascii_case(\"kib\") {\n            *value *= KIBI_LIMIT_F64;\n            query.pop_front();\n        } else if potential_unit.eq_ignore_ascii_case(\"b\") {\n            query.pop_front();\n        }\n    }\n}\n\n/// Either contains a further `Or` recursively, or an attribute that can be queried, possibly as\n/// part of a larger query.\n///\n/// In theory, this can be made generic to work on all table types, though for now, it's\n/// hardcoded for processes.\n#[derive(Debug)]\npub(super) enum Prefix {\n    Or(Box<Or>),\n    Attribute(ProcessAttribute),\n}\n\nimpl Prefix {\n    pub(super) fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {\n        match self {\n            Prefix::Or(or) => or.check(process, is_using_command),\n            Prefix::Attribute(attribute) => attribute.check(process, is_using_command),\n        }\n    }\n\n    fn process_in_quotes(\n        query: &mut VecDeque<String>, options: &QueryOptions,\n    ) -> QueryResult<Self> {\n        if let Some(queue_top) = query.pop_front() {\n            if queue_top == \"\\\"\" {\n                // This means we hit something like \"\". Return an empty prefix, and to deal\n                // with the close quote checker, add one to the top of the\n                // stack. Ugly fix but whatever.\n                query.push_front(\"\\\"\".to_string());\n\n                Ok(Prefix::Attribute(ProcessAttribute::Empty))\n            } else {\n                let mut intern_string = vec![queue_top];\n\n                // TODO: I think this should consume the quote...? Might need to check the other spot\n                // we process quotes.\n                while let Some(next_str) = query.front() {\n                    if next_str == \"\\\"\" {\n                        break;\n                    } else {\n                        intern_string.push(query.pop_front().expect(\"we just peeked at the front\"));\n                    }\n                }\n\n                let quoted_string = intern_string.join(\" \");\n\n                Ok(Prefix::Attribute(new_string_attribute(\n                    PrefixType::Name,\n                    &quoted_string,\n                    options,\n                )?))\n            }\n        } else {\n            // Uh oh, there's nothing left in the stack, but we're inside quotes!\n            Err(QueryError::new(\"Missing closing quotation\"))\n        }\n    }\n}\n\nimpl QueryProcessor for Prefix {\n    fn process(query: &mut VecDeque<String>, options: &QueryOptions) -> QueryResult<Self>\n    where\n        Self: Sized,\n    {\n        if let Some(curr) = query.pop_front() {\n            if curr == \"(\" {\n                if query.is_empty() {\n                    return Err(QueryError::new(\"Missing closing parentheses\"));\n                }\n\n                let mut list_of_ors = VecDeque::new();\n\n                while let Some(in_paren_query_top) = query.front() {\n                    if in_paren_query_top != \")\" {\n                        list_of_ors.push_back(Or::process(query, options)?);\n                    } else {\n                        break;\n                    }\n                }\n\n                let Some(front) = list_of_ors.pop_front() else {\n                    return Err(QueryError::new(\"No values within parentheses group\"));\n                };\n\n                // Now convert this back to a OR...\n                // TODO: is there a better way to do this than converting it?\n                let initial_or = Or {\n                    lhs: And {\n                        lhs: Prefix::Or(Box::new(front)),\n                        rhs: None,\n                    },\n                    rhs: None,\n                };\n                let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or {\n                    lhs: And {\n                        lhs: Prefix::Or(Box::new(lhs)),\n                        rhs: Some(Box::new(Prefix::Or(Box::new(rhs)))),\n                    },\n                    rhs: None,\n                });\n\n                return if let Some(close_paren) = query.pop_front() {\n                    if close_paren == \")\" {\n                        Ok(Prefix::Or(Box::new(returned_or)))\n                    } else {\n                        Err(QueryError::new(\"Missing closing parentheses\"))\n                    }\n                } else {\n                    Err(QueryError::new(\"Missing closing parentheses\"))\n                };\n            } else if curr == \")\" {\n                return Err(QueryError::new(\"Missing opening parentheses\"));\n            } else if curr == \"\\\"\" {\n                // Similar to parentheses, trap and check for missing closing quotes.  Note,\n                // however, that we will DIRECTLY call another process_prefix\n                // call...\n\n                let prefix = Prefix::process_in_quotes(query, options)?;\n                return if let Some(close_quote) = query.pop_front() {\n                    if close_quote == \"\\\"\" {\n                        Ok(prefix)\n                    } else {\n                        Err(QueryError::new(\"Missing closing quotation\"))\n                    }\n                } else {\n                    Err(QueryError::new(\"Missing closing quotation\"))\n                };\n            } else {\n                // Get prefix type.\n                let prefix_type = curr.parse::<PrefixType>()?;\n\n                // TODO: Separate these cases here and below.\n                let content = if let PrefixType::Name = prefix_type {\n                    Some(curr)\n                } else {\n                    query.pop_front()\n                };\n\n                if let Some(content) = content {\n                    match &prefix_type {\n                        PrefixType::Name => {\n                            return Ok(Prefix::Attribute(new_string_attribute(\n                                prefix_type,\n                                &content,\n                                options,\n                            )?));\n                        }\n                        PrefixType::Pid | PrefixType::State | PrefixType::User => {\n                            // We have to check if someone put an \"=\"...\n                            if content == \"=\" {\n                                // Check next string if possible\n                                if let Some(string_value) = query.pop_front() {\n                                    // TODO: [Query] Need to consider the following cases:\n                                    // - (test)\n                                    // - (test\n                                    // - test)\n                                    // These are split into 2 to 3 different strings due to\n                                    // parentheses being\n                                    // delimiters in our query system.\n                                    //\n                                    // Do we want these to be valid?  They should, as a string,\n                                    // right?\n\n                                    // We also must check if this value is wrapped in quotes!\n                                    let final_value = if string_value == \"\\\"\" {\n                                        let mut intern_string = vec![];\n\n                                        // Keep parsing until we either hit another quotation or we error.\n                                        while let Some(next_string) = query.pop_front() {\n                                            if next_string == \"\\\"\" {\n                                                break;\n                                            }\n\n                                            intern_string.push(next_string);\n                                        }\n\n                                        intern_string.join(\" \")\n                                    } else {\n                                        string_value\n                                    };\n\n                                    return Ok(Prefix::Attribute(new_string_attribute(\n                                        prefix_type,\n                                        &final_value,\n                                        options,\n                                    )?));\n                                }\n                            } else {\n                                return Ok(Prefix::Attribute(new_string_attribute(\n                                    prefix_type,\n                                    &content,\n                                    options,\n                                )?));\n                            }\n                        }\n                        PrefixType::Time => {\n                            let mut condition: Option<QueryComparison> = None;\n                            let mut duration_string: Option<String> = None;\n\n                            if content == \"=\" {\n                                condition = Some(QueryComparison::Equal);\n                                duration_string = query.pop_front();\n                            } else if content == \">\" || content == \"<\" {\n                                if let Some(queue_next) = query.pop_front() {\n                                    if queue_next == \"=\" {\n                                        condition = Some(if content == \">\" {\n                                            QueryComparison::GreaterOrEqual\n                                        } else {\n                                            QueryComparison::LessOrEqual\n                                        });\n                                        duration_string = query.pop_front();\n                                    } else {\n                                        condition = Some(if content == \">\" {\n                                            QueryComparison::Greater\n                                        } else {\n                                            QueryComparison::Less\n                                        });\n                                        duration_string = Some(queue_next);\n                                    }\n                                } else {\n                                    return Err(QueryError::missing_value());\n                                }\n                            }\n\n                            if let Some(condition) = condition {\n                                let duration = parse_duration(\n                                    &duration_string.ok_or(QueryError::missing_value())?,\n                                )\n                                .map_err(|err| QueryError::new(err.to_string()))?;\n\n                                return Ok(Prefix::Attribute(new_time_attribute(\n                                    prefix_type,\n                                    TimeQuery {\n                                        condition,\n                                        duration,\n                                    },\n                                )?));\n                            }\n                        }\n                        _ => {\n                            // Assume it's some numerical value.\n                            // Now we gotta parse the content... yay.\n\n                            let mut condition: Option<QueryComparison> = None;\n                            let mut value: Option<f64> = None;\n\n                            // TODO: Jeez, what the heck did I write here... add some tests and\n                            // clean this up in the future.\n                            if content == \"=\" {\n                                condition = Some(QueryComparison::Equal);\n                                if let Some(queue_next) = query.pop_front() {\n                                    value = queue_next.parse::<f64>().ok();\n                                } else {\n                                    return Err(QueryError::missing_value());\n                                }\n                            } else if content == \">\" || content == \"<\" {\n                                // We also have to check if the next string is an \"=\"...\n                                if let Some(queue_next) = query.pop_front() {\n                                    if queue_next == \"=\" {\n                                        condition = Some(if content == \">\" {\n                                            QueryComparison::GreaterOrEqual\n                                        } else {\n                                            QueryComparison::LessOrEqual\n                                        });\n                                        if let Some(queue_next_next) = query.pop_front() {\n                                            value = queue_next_next.parse::<f64>().ok();\n                                        } else {\n                                            return Err(QueryError::missing_value());\n                                        }\n                                    } else {\n                                        condition = Some(if content == \">\" {\n                                            QueryComparison::Greater\n                                        } else {\n                                            QueryComparison::Less\n                                        });\n                                        value = queue_next.parse::<f64>().ok();\n                                    }\n                                } else {\n                                    return Err(QueryError::missing_value());\n                                }\n                            }\n\n                            if let Some(condition) = condition {\n                                if let Some(read_value) = value {\n                                    // Note that the values *might* have a unit or need to be parsed\n                                    // differently based on the\n                                    // prefix type!\n\n                                    // TODO: Support this without spaces?\n\n                                    let mut value = read_value;\n\n                                    match prefix_type {\n                                        PrefixType::MemBytes\n                                        | PrefixType::ReadPerSecond\n                                        | PrefixType::WritePerSecond\n                                        | PrefixType::TotalRead\n                                        | PrefixType::TotalWrite => {\n                                            process_prefix_units(query, &mut value);\n                                        }\n                                        #[cfg(feature = \"gpu\")]\n                                        PrefixType::GpuMemoryBytes => {\n                                            process_prefix_units(query, &mut value);\n                                        }\n                                        _ => {}\n                                    }\n\n                                    return Ok(Prefix::Attribute(new_numerical_attribute(\n                                        prefix_type,\n                                        NumericalQuery { condition, value },\n                                    )?));\n                                }\n                            }\n                        }\n                    }\n                } else {\n                    return Err(QueryError::new(\"Missing argument for search prefix\"));\n                }\n            }\n        }\n\n        // TODO: Give more information here (e.g. closest query?), though this is moreso meant as a fallback.\n        Err(QueryError::new(\"Invalid query\"))\n    }\n}\n"
  },
  {
    "path": "src/widgets/process_table/query.rs",
    "content": "//! How we query processes.\n//!\n//! Yes, this is a hand-rolled parser. I originally wrote this back in uni where writing\n//! a parser was basically a thing I did every year, and parsing crate options were not\n//! as good as they are now. This will be rewritten as time goes on, though.\n\nmod and;\nmod attribute;\nmod error;\nmod or;\nmod prefix;\n\nuse std::{collections::VecDeque, time::Duration};\n\nuse and::And;\nuse attribute::ProcessAttribute;\nuse error::{QueryError, QueryResult};\nuse or::Or;\nuse prefix::Prefix;\nuse regex::Regex;\n\nuse crate::{collection::processes::ProcessHarvest, multi_eq_ignore_ascii_case};\n\nconst DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\\\"'];\nconst COMPARISON_LIST: [&str; 3] = [\">\", \"=\", \"<\"];\n\n/// A node type that can take a query and read it, advancing the current read state\n/// and returning an instance of the node.\ntrait QueryProcessor {\n    fn process(query: &mut VecDeque<String>, regex_options: &QueryOptions) -> QueryResult<Self>\n    where\n        Self: Sized;\n}\n\n/// Process a new regex given a `base` string and some settings.\n///\n/// TODO: Push this into a struct so I don't have to throw the options around so much.\nfn new_regex(base: &str, regex_options: &QueryOptions) -> QueryResult<Regex> {\n    let QueryOptions {\n        whole_word: is_searching_whole_word,\n        ignore_case: is_ignoring_case,\n        use_regex: is_searching_with_regex,\n    } = regex_options;\n    let escaped_regex: String; // Needed for ownership reasons.\n\n    let final_regex_string = &format!(\n        \"{}{}{}{}\",\n        if *is_searching_whole_word { \"^\" } else { \"\" },\n        if *is_ignoring_case { \"(?i)\" } else { \"\" },\n        if !(*is_searching_with_regex) {\n            escaped_regex = regex::escape(base);\n            &escaped_regex\n        } else {\n            base\n        },\n        if *is_searching_whole_word { \"$\" } else { \"\" },\n    );\n\n    Ok(Regex::new(final_regex_string)?)\n}\n\n/// Options when creating a new query.\n#[derive(PartialEq, Eq)]\npub struct QueryOptions {\n    /// Whether we only allow matches on the entire word.\n    pub whole_word: bool,\n\n    /// Whether to ignore case-sensitivity when searching. On by default.\n    pub ignore_case: bool,\n\n    /// Whether we should use regex syntax when searching. If not set, then it\n    /// should treat everything as a literal string.\n    pub use_regex: bool,\n}\n\nimpl Default for QueryOptions {\n    fn default() -> Self {\n        Self {\n            ignore_case: true,\n            whole_word: false,\n            use_regex: false,\n        }\n    }\n}\n\n/// In charge of parsing the given query, case-insensitive, possibly marked\n/// by a prefix. For example:\n///\n/// - Process names: No prefix required, can use regex, match word, or case.\n///   Enclosing anything, including prefixes, in quotes, means we treat it as an\n///   entire process rather than a prefix.\n/// - PIDs: Use prefix `pid`, can use regex or match word.\n/// - CPU: Use prefix `cpu`.\n/// - MEM: Use prefix `mem`.\n/// - STATE: Use prefix `state`.\n/// - USER: Use prefix `user`.\n/// - Read/s: Use prefix `r`.\n/// - Write/s: Use prefix `w`.\n/// - Total read: Use prefix `read`.\n/// - Total write: Use prefix `write`.\n///\n/// For queries, whitespaces are our delimiters.  We will merge together any\n/// adjacent non-prefixed or quoted elements after splitting to treat as process\n/// names. Furthermore, we want to support boolean joiners like AND and OR, and\n/// brackets.\npub(crate) fn parse_query(search_query: &str, options: &QueryOptions) -> QueryResult<ProcessQuery> {\n    fn process_string_to_filter(\n        query: &mut VecDeque<String>, options: &QueryOptions,\n    ) -> QueryResult<ProcessQuery> {\n        let lhs = Or::process(query, options)?;\n        let mut list_of_ors = vec![lhs];\n\n        while query.front().is_some() {\n            list_of_ors.push(Or::process(query, options)?);\n        }\n\n        Ok(ProcessQuery { query: list_of_ors })\n    }\n\n    let mut split_query = VecDeque::new();\n\n    search_query.split_whitespace().for_each(|s| {\n        // From https://stackoverflow.com/a/56923739 get a split but include the parentheses\n        let mut last = 0;\n        for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {\n            if last != index {\n                split_query.push_back(s[last..index].to_owned());\n            }\n            split_query.push_back(matched.to_owned());\n            last = index + matched.len();\n        }\n        if last < s.len() {\n            split_query.push_back(s[last..].to_owned());\n        }\n    });\n\n    process_string_to_filter(&mut split_query, options)\n}\n\n#[derive(Debug)]\npub struct ProcessQuery {\n    /// Remember, AND > OR, but AND must come after OR when we parse.\n    query: Vec<Or>,\n}\n\nimpl ProcessQuery {\n    pub(crate) fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {\n        self.query\n            .iter()\n            .all(|ok| ok.check(process, is_using_command))\n    }\n}\n\n#[derive(Debug)]\nenum PrefixType {\n    Pid,\n    CpuPercentage,\n    MemBytes,\n    MemPercentage,\n    ReadPerSecond,\n    WritePerSecond,\n    TotalRead,\n    TotalWrite,\n    Name,\n    State,\n    User,\n    Time,\n    #[cfg(unix)]\n    Nice,\n    Priority,\n    #[cfg(feature = \"gpu\")]\n    GpuPercentage,\n    #[cfg(feature = \"gpu\")]\n    GpuMemoryBytes,\n    #[cfg(feature = \"gpu\")]\n    GpuMemoryPercentage,\n}\n\nimpl std::str::FromStr for PrefixType {\n    type Err = QueryError;\n\n    fn from_str(s: &str) -> QueryResult<Self> {\n        use PrefixType::*;\n\n        // TODO: Didn't add mem_bytes, total_read, and total_write\n        // for now as it causes help to be clogged.\n\n        let mut result = Name;\n        if multi_eq_ignore_ascii_case!(s, \"cpu\" | \"cpu%\") {\n            result = CpuPercentage;\n        } else if multi_eq_ignore_ascii_case!(s, \"mem\" | \"mem%\") {\n            result = MemPercentage;\n        } else if multi_eq_ignore_ascii_case!(s, \"memb\") {\n            result = MemBytes;\n        } else if multi_eq_ignore_ascii_case!(s, \"read\" | \"r/s\" | \"rps\") {\n            result = ReadPerSecond;\n        } else if multi_eq_ignore_ascii_case!(s, \"write\" | \"w/s\" | \"wps\") {\n            result = WritePerSecond;\n        } else if multi_eq_ignore_ascii_case!(s, \"tread\" | \"t.read\") {\n            result = TotalRead;\n        } else if multi_eq_ignore_ascii_case!(s, \"twrite\" | \"t.write\") {\n            result = TotalWrite;\n        } else if multi_eq_ignore_ascii_case!(s, \"pid\") {\n            result = Pid;\n        } else if multi_eq_ignore_ascii_case!(s, \"state\") {\n            result = State;\n        } else if multi_eq_ignore_ascii_case!(s, \"user\") {\n            result = User;\n        } else if multi_eq_ignore_ascii_case!(s, \"time\") {\n            result = Time;\n        } else if multi_eq_ignore_ascii_case!(s, \"nice\") {\n            #[cfg(unix)]\n            {\n                result = Nice;\n            }\n        } else if multi_eq_ignore_ascii_case!(s, \"priority\") {\n            result = Priority;\n        }\n        #[cfg(feature = \"gpu\")]\n        {\n            if multi_eq_ignore_ascii_case!(s, \"gmem\") {\n                result = GpuMemoryBytes;\n            } else if multi_eq_ignore_ascii_case!(s, \"gmem%\") {\n                result = GpuMemoryPercentage;\n            } else if multi_eq_ignore_ascii_case!(s, \"gpu%\") {\n                result = GpuPercentage;\n            }\n        }\n        Ok(result)\n    }\n}\n\n#[derive(Debug)]\nenum QueryComparison {\n    Equal,\n    Less,\n    Greater,\n    LessOrEqual,\n    GreaterOrEqual,\n}\n\n#[derive(Debug)]\nstruct NumericalQuery {\n    condition: QueryComparison,\n    value: f64,\n}\n\nimpl NumericalQuery {\n    /// Compare `lhs` to the value in the query as `rhs`.\n    fn check<I: Into<f64>>(&self, lhs: I) -> bool {\n        let lhs: f64 = lhs.into();\n        let rhs: f64 = self.value;\n\n        match self.condition {\n            QueryComparison::Equal => (lhs - rhs).abs() < f64::EPSILON,\n            QueryComparison::Less => lhs < rhs,\n            QueryComparison::Greater => lhs > rhs,\n            QueryComparison::LessOrEqual => lhs <= rhs,\n            QueryComparison::GreaterOrEqual => lhs >= rhs,\n        }\n    }\n}\n\n#[derive(Debug)]\nstruct TimeQuery {\n    condition: QueryComparison,\n    duration: Duration,\n}\n\nimpl TimeQuery {\n    /// Compare `lhs` to the value in the query as `rhs`.\n    fn check(&self, lhs: Duration) -> bool {\n        let rhs = self.duration;\n\n        match self.condition {\n            QueryComparison::Equal => lhs == rhs,\n            QueryComparison::Less => lhs < rhs,\n            QueryComparison::Greater => lhs > rhs,\n            QueryComparison::LessOrEqual => lhs <= rhs,\n            QueryComparison::GreaterOrEqual => lhs >= rhs,\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    fn simple_process(name: &str) -> ProcessHarvest {\n        ProcessHarvest {\n            name: name.into(),\n            ..Default::default()\n        }\n    }\n\n    fn parse_query_no_options(query: &str) -> QueryResult<ProcessQuery> {\n        parse_query(\n            query,\n            &QueryOptions {\n                whole_word: false,\n                ignore_case: false,\n                use_regex: false,\n            },\n        )\n    }\n\n    #[test]\n    fn basic_query() {\n        let query = parse_query_no_options(\"test\").unwrap();\n\n        let exact_match = simple_process(\"test\");\n        let contains = simple_process(\"test string\");\n        let invalid = simple_process(\"no\");\n\n        assert!(query.check(&exact_match, false));\n        assert!(query.check(&contains, false));\n        assert!(!query.check(&invalid, false));\n    }\n\n    #[test]\n    fn basic_or_query() {\n        let query = parse_query_no_options(\"a or b\").unwrap();\n\n        let a = simple_process(\"a\");\n        let b = simple_process(\"b\");\n        let invalid = simple_process(\"c\");\n\n        assert!(query.check(&a, false));\n        assert!(query.check(&b, false));\n        assert!(!query.check(&invalid, false));\n    }\n\n    #[test]\n    fn basic_and_query() {\n        let query = parse_query_no_options(\"a and b\").unwrap();\n\n        let a = simple_process(\"a\");\n        let b = simple_process(\"b\");\n        let c = simple_process(\"c\");\n        let a_and_b = simple_process(\"a and b\");\n\n        assert!(!query.check(&a, false));\n        assert!(!query.check(&b, false));\n        assert!(!query.check(&c, false));\n        assert!(query.check(&a_and_b, false));\n    }\n\n    #[test]\n    fn implied_and_query() {\n        let query = parse_query_no_options(\"a b c\").unwrap();\n\n        let a = simple_process(\"a\");\n        let b = simple_process(\"b\");\n        let c = simple_process(\"c\");\n        let all = simple_process(\"a b c\");\n\n        assert!(!query.check(&a, false));\n        assert!(!query.check(&b, false));\n        assert!(!query.check(&c, false));\n        assert!(query.check(&all, false));\n    }\n\n    /// Ensure that quoted keywords are treated as strings. In this case, rather than `\"a\" OR \"b\"`, it should be treated\n    /// as the string `\"a or b\"`.\n    #[test]\n    fn quoted_query() {\n        let query = parse_query_no_options(\"a \\\"or\\\" b\").unwrap();\n\n        let a = simple_process(\"a\");\n        let b = simple_process(\"b\");\n        let or = simple_process(\"or\");\n        let valid = simple_process(\"a \\\"or\\\" b\"); // This is valid as the query is \"match a word with a, or, and b\".\n        let valid_2 = simple_process(\"a or b\");\n        let valid_3 = simple_process(\"a \\\"or\\\" b \\\"or\\\" c\");\n\n        assert!(!query.check(&a, false));\n        assert!(!query.check(&b, false));\n        assert!(!query.check(&or, false));\n        assert!(query.check(&valid, false));\n        assert!(query.check(&valid_2, false));\n        assert!(query.check(&valid_3, false));\n    }\n\n    /// Ensure that multi-word quoted keywords are treated as strings. In this case, rather than `\"a\" OR \"b\"`, it should be treated\n    /// as the string `\"a or b\"`.\n    #[test]\n    fn quoted_multi_word_query() {\n        let query = parse_query_no_options(\"\\\"a or b\\\"\").unwrap();\n\n        let a = simple_process(\"a\");\n        let b = simple_process(\"b\");\n        let or = simple_process(\"or\");\n        let valid = simple_process(\"a or b\");\n        let valid_2 = simple_process(\"a or b \\\"or\\\" c\");\n        let invalid_no_regex = simple_process(\"a \\\"or\\\" b\"); // Invalid now as the query is one big string!\n\n        assert!(!query.check(&a, false));\n        assert!(!query.check(&b, false));\n        assert!(!query.check(&or, false));\n        assert!(query.check(&valid, false));\n        assert!(query.check(&valid_2, false));\n        assert!(!query.check(&invalid_no_regex, false));\n    }\n\n    #[test]\n    fn basic_cpu_query() {\n        let query = parse_query_no_options(\"cpu > 50\").unwrap();\n\n        let mut over = simple_process(\"a\");\n        over.cpu_usage_percent = 60.0;\n\n        let mut under = simple_process(\"a\");\n        under.cpu_usage_percent = 40.0;\n\n        let mut exact = simple_process(\"a\");\n        exact.cpu_usage_percent = 50.0;\n\n        assert!(query.check(&over, false));\n        assert!(!query.check(&under, false));\n        assert!(!query.check(&exact, false));\n    }\n\n    #[test]\n    fn basic_mem_query() {\n        let query = parse_query_no_options(\"memb > 1 GiB\").unwrap();\n\n        let mut over = simple_process(\"a\");\n        over.mem_usage = 2 * 1024 * 1024 * 1024;\n\n        let mut under = simple_process(\"a\");\n        under.mem_usage = 0;\n\n        let mut exact = simple_process(\"a\");\n        exact.mem_usage = 1024 * 1024 * 1024;\n\n        assert!(query.check(&over, false));\n        assert!(!query.check(&under, false));\n        assert!(!query.check(&exact, false));\n    }\n\n    /// This test sees if parentheses work.\n    #[test]\n    fn nested_query_1() {\n        let query = parse_query_no_options(\"(a or b) and (c or a)\").unwrap();\n\n        let a = simple_process(\"a\");\n        let b = simple_process(\"b\");\n        let c = simple_process(\"c\");\n        let d = simple_process(\"d\");\n\n        assert!(query.check(&a, false));\n        assert!(!query.check(&b, false));\n        assert!(!query.check(&c, false));\n        assert!(!query.check(&d, false));\n    }\n\n    /// This test sees if parentheses and mixed query types work.\n    #[test]\n    fn nested_query_2() {\n        let query = parse_query_no_options(\"(cpu > 10 or cpu < 5) and (c or a)\").unwrap();\n\n        let mut a_valid_1 = simple_process(\"a\");\n        a_valid_1.cpu_usage_percent = 100.0;\n\n        let mut a_valid_2 = simple_process(\"a\");\n        a_valid_2.cpu_usage_percent = 1.0;\n\n        let mut a_invalid = simple_process(\"a\");\n        a_invalid.cpu_usage_percent = 6.0;\n\n        let mut c = simple_process(\"c\");\n        c.cpu_usage_percent = 50.0;\n\n        let mut b = simple_process(\"b\");\n        b.cpu_usage_percent = 50.0;\n\n        let mut d = simple_process(\"d\");\n        d.cpu_usage_percent = 6.0;\n\n        assert!(query.check(&a_valid_1, false));\n        assert!(query.check(&a_valid_2, false));\n        assert!(query.check(&c, false));\n\n        assert!(!query.check(&a_invalid, false));\n        assert!(!query.check(&b, false));\n        assert!(!query.check(&d, false));\n    }\n\n    /// This test adds a further layer of nesting to consider.\n    #[test]\n    fn nested_query_3() {\n        let query =\n            parse_query_no_options(\"((cpu > 10 or cpu < 5) or d) and ((c or a) or d)\").unwrap();\n\n        let mut a_valid_1 = simple_process(\"a\");\n        a_valid_1.cpu_usage_percent = 100.0;\n\n        let mut a_valid_2 = simple_process(\"a\");\n        a_valid_2.cpu_usage_percent = 1.0;\n\n        let mut a_invalid = simple_process(\"a\");\n        a_invalid.cpu_usage_percent = 6.0;\n\n        let mut c = simple_process(\"c\");\n        c.cpu_usage_percent = 50.0;\n\n        let mut b = simple_process(\"b\");\n        b.cpu_usage_percent = 50.0;\n\n        let mut d = simple_process(\"d\");\n        d.cpu_usage_percent = 6.0;\n\n        assert!(query.check(&a_valid_1, false));\n        assert!(query.check(&a_valid_2, false));\n        assert!(query.check(&c, false));\n        assert!(query.check(&d, false));\n\n        assert!(!query.check(&a_invalid, false));\n        assert!(!query.check(&b, false));\n    }\n\n    #[test]\n    fn ambiguous_precedence_1() {\n        let query = parse_query_no_options(\"a and b or c\").unwrap();\n\n        let a = simple_process(\"a\");\n        let b = simple_process(\"b\");\n        let c = simple_process(\"c\");\n\n        assert!(!query.check(&a, false));\n        assert!(!query.check(&b, false));\n        assert!(query.check(&c, false));\n    }\n\n    #[test]\n    fn ambiguous_precedence_2() {\n        let query = parse_query_no_options(\"a or b and c\").unwrap();\n\n        let a = simple_process(\"a\");\n        let b = simple_process(\"b\");\n        let c = simple_process(\"c\");\n\n        assert!(query.check(&a, false));\n        assert!(!query.check(&b, false));\n        assert!(!query.check(&c, false));\n    }\n\n    /// Test if a complicated query even parses.\n    #[test]\n    fn parse_complicated_query() {\n        parse_query_no_options(\n            \"cpu > 10.5 AND (memb = 1 MiB || state = sleeping) and (a or b) and (read >= 0 or write >= 0)\",\n\n        )\n        .unwrap();\n    }\n\n    /// Test empty quotes works.\n    #[test]\n    fn parse_empty_quotes() {\n        parse_query_no_options(\"\\\"\\\"\").unwrap();\n        parse_query_no_options(\"\\\"\\\"\\\"\\\"\").unwrap();\n        parse_query_no_options(\"\\\"\\\" OR \\\"\\\"\").unwrap();\n    }\n\n    #[test]\n    fn search_empty_quotes() {\n        let a = parse_query_no_options(\"\\\"\\\"\").unwrap();\n        let b = parse_query_no_options(\"\\\"\\\" OR test\").unwrap();\n\n        let process = simple_process(\"test\");\n\n        assert!(a.check(&process, false));\n        assert!(b.check(&process, false));\n    }\n\n    /// Test unfinished quotes error.\n    #[test]\n    fn parse_unfinished_quotes() {\n        parse_query_no_options(\"\\\"\").unwrap_err();\n        parse_query_no_options(\"\\\"asdf\").unwrap_err();\n        parse_query_no_options(\"asdf\\\"\").unwrap_err();\n    }\n\n    /// Test a fix for a bug with closing quotations. The problem seems to arise from quotes being used as an argument\n    /// to a prefix... but this should probably be valid.\n    #[test]\n    fn parse_nested_closing_quotes() {\n        parse_query_no_options(\"state = \\\"test\\\"\").unwrap();\n        parse_query_no_options(\"state = \\\"2 words\\\"\").unwrap();\n        parse_query_no_options(\"(memb = 1 MiB || state = \\\"test\\\")\").unwrap();\n        parse_query_no_options(\"(memb = 1 MiB || state = \\\"2 words\\\")\").unwrap();\n    }\n\n    // TODO: Add this after fixed.\n    // /// Test if units can ignore spaces from their preceding value.\n    // #[test]\n    // fn units_with_and_without_spaces() {}\n\n    #[test]\n    fn invalid_uncompleted_queries_1() {\n        parse_query_no_options(\"state =\").unwrap_err();\n        parse_query_no_options(\"a or\").unwrap_err();\n        parse_query_no_options(\"a >\").unwrap_err();\n    }\n\n    #[track_caller]\n    fn invalid_lhs_rhs(op: &str) {\n        parse_query_no_options(&format!(\"a {op} asdf = 100\")).unwrap_err();\n        parse_query_no_options(&format!(\"asdf = 100 {op} b\")).unwrap_err();\n        parse_query_no_options(&format!(\"a {op} asdf = 100 {op} b\")).unwrap_err();\n\n        parse_query_no_options(&format!(\"asdf = 100 {op} bsdf = \\\"\")).unwrap_err();\n        parse_query_no_options(&format!(\"a {op} bsdf = \\\"\")).unwrap_err();\n    }\n\n    #[test]\n    fn invalid_or() {\n        invalid_lhs_rhs(\"OR\");\n        invalid_lhs_rhs(\"||\");\n    }\n\n    #[test]\n    fn invalid_and() {\n        invalid_lhs_rhs(\"AND\");\n        invalid_lhs_rhs(\"&&\");\n        invalid_lhs_rhs(\"\");\n    }\n\n    // /// Test keywords.\n    // ///\n    // /// TODO: Should these be invalid...?\n    // #[test]\n    // fn invalid_query_x() {\n    //     parse_query_no_options(\"or\").unwrap_err();\n    //     parse_query_no_options(\"and\").unwrap_err();\n    //     parse_query_no_options(\"a or >\").unwrap_err();\n    //     parse_query_no_options(\"a and >\").unwrap_err();\n    // }\n\n    #[test]\n    fn test_command_check() {\n        let query = parse_query_no_options(\"command\").unwrap();\n\n        let mut process_a = simple_process(\"test\");\n        process_a.command = \"command\".into();\n\n        let mut process_b = simple_process(\"test\");\n        process_b.command = \"no\".into();\n\n        assert!(query.check(&process_a, true));\n        assert!(!query.check(&process_b, true));\n    }\n\n    #[test]\n    fn test_non_ascii_only_1() {\n        let query = parse_query_no_options(\"食\").unwrap();\n\n        let process_a = simple_process(\"施氏食獅史\");\n        let process_b = simple_process(\"沒有\");\n\n        assert!(query.check(&process_a, false));\n        assert!(!query.check(&process_b, false));\n    }\n\n    #[test]\n    fn test_non_ascii_only_2() {\n        let query = parse_query_no_options(\"परीक्षा\").unwrap();\n\n        let process_a = simple_process(\"परीक्षा\");\n        let process_b = simple_process(\"उपलब्ध नहीं है\");\n\n        assert!(query.check(&process_a, false));\n        assert!(!query.check(&process_b, false));\n    }\n\n    #[test]\n    fn test_non_ascii_only_3() {\n        let query = parse_query_no_options(\"🇨🇦\").unwrap();\n\n        let process_a = simple_process(\"🇨🇦\");\n        let process_b = simple_process(\"❤️🇨🇦❤️\");\n        let process_c = simple_process(\"❤️\");\n\n        assert!(query.check(&process_a, false));\n        assert!(query.check(&process_b, false));\n        assert!(!query.check(&process_c, false));\n    }\n\n    #[test]\n    fn test_non_ascii_only_4() {\n        let query = parse_query_no_options(\"獅 or 狮\").unwrap();\n\n        let process_a = simple_process(\"施氏食獅史\");\n        let process_b = simple_process(\"施氏食狮史\");\n        let process_c = simple_process(\"沒有\");\n\n        assert!(query.check(&process_a, false));\n        assert!(query.check(&process_b, false));\n        assert!(!query.check(&process_c, false));\n    }\n\n    #[test]\n    fn test_invalid_non_ascii() {\n        parse_query_no_options(\"cpu = 食\").unwrap_err();\n    }\n\n    #[test]\n    fn test_mixed_unicode() {\n        let query = parse_query_no_options(\"食 or test\").unwrap();\n\n        let process_a = simple_process(\"施氏食獅史\");\n        let process_b = simple_process(\"test\");\n        let process_c = simple_process(\"施氏食獅史test\");\n        let process_d = simple_process(\"沒有\");\n        let process_e = simple_process(\"nope\");\n\n        assert!(query.check(&process_a, false));\n        assert!(query.check(&process_b, false));\n        assert!(query.check(&process_c, false));\n        assert!(!query.check(&process_d, false));\n        assert!(!query.check(&process_e, false));\n    }\n\n    #[test]\n    fn test_regex_1() {\n        let query = parse_query(\n            \"(a|b)\",\n            &QueryOptions {\n                whole_word: false,\n                ignore_case: true,\n                use_regex: true,\n            },\n        )\n        .unwrap();\n\n        let process_a = simple_process(\"abc\");\n        let process_b = simple_process(\"test\");\n\n        assert!(query.check(&process_a, false));\n        assert!(!query.check(&process_b, false));\n    }\n\n    #[test]\n    fn test_regex_2() {\n        let query = parse_query(\n            \"^a.*z$\",\n            &QueryOptions {\n                whole_word: false,\n                ignore_case: true,\n                use_regex: true,\n            },\n        )\n        .unwrap();\n\n        let process_a = simple_process(\"atoz\");\n        let process_b = simple_process(\"atob\");\n        let process_c = simple_process(\"ytoz\");\n        let process_d = simple_process(\"atozoops\");\n\n        assert!(query.check(&process_a, false));\n        assert!(!query.check(&process_b, false));\n        assert!(!query.check(&process_c, false));\n        assert!(!query.check(&process_d, false));\n    }\n\n    #[test]\n    fn test_whole_word_1() {\n        let query = parse_query(\n            \"test\",\n            &QueryOptions {\n                whole_word: true,\n                ignore_case: true,\n                use_regex: false,\n            },\n        )\n        .unwrap();\n\n        let process_a = simple_process(\"test\");\n        let process_b = simple_process(\"testa\");\n        let process_c = simple_process(\"atest\");\n\n        assert!(query.check(&process_a, false));\n        assert!(!query.check(&process_b, false));\n        assert!(!query.check(&process_c, false));\n    }\n\n    #[test]\n    fn test_case_sensitive_1() {\n        let query = parse_query(\n            \"tEsT\",\n            &QueryOptions {\n                whole_word: false,\n                ignore_case: false,\n                use_regex: false,\n            },\n        )\n        .unwrap();\n\n        let process_a = simple_process(\"tEsT\");\n        let process_b = simple_process(\"tEsT a\");\n        let process_c = simple_process(\"a tEsT\");\n\n        assert!(query.check(&process_a, false));\n        assert!(query.check(&process_b, false));\n        assert!(query.check(&process_c, false));\n\n        let process_d = simple_process(\"test\");\n        let process_e = simple_process(\"test a\");\n        let process_f = simple_process(\"a test\");\n\n        assert!(!query.check(&process_d, false));\n        assert!(!query.check(&process_e, false));\n        assert!(!query.check(&process_f, false));\n    }\n\n    #[cfg(feature = \"gpu\")]\n    #[test]\n    fn test_gpu_queries() {\n        let mem = parse_query_no_options(\"gmem > 50 b\").unwrap();\n        let mem_percent = parse_query_no_options(\"gmem% = 50\").unwrap();\n        let use_percent = parse_query_no_options(\"gpu% = 50\").unwrap();\n\n        let mut process_a = simple_process(\"test\");\n        process_a.gpu_mem = 100;\n        process_a.gpu_mem_percent = 50.0;\n        process_a.gpu_util = 50;\n\n        assert!(mem.check(&process_a, false));\n        assert!(mem_percent.check(&process_a, false));\n        assert!(use_percent.check(&process_a, false));\n\n        let mut process_b = simple_process(\"test\");\n        process_b.gpu_mem = 0;\n        process_b.gpu_mem_percent = 10.0;\n        process_b.gpu_util = 10;\n\n        assert!(!mem.check(&process_b, false));\n        assert!(!mem_percent.check(&process_b, false));\n        assert!(!use_percent.check(&process_b, false));\n    }\n\n    /// Test GPU queries that involve invalid string comparisons.\n    #[cfg(feature = \"gpu\")]\n    #[test]\n    fn test_invalid_gpu_queries() {\n        parse_query_no_options(\"gmem = \\\"what\\\"\").unwrap_err();\n        parse_query_no_options(\"gmem% = \\\"the\\\"\").unwrap_err();\n        parse_query_no_options(\"gpu% = \\\"heck\\\"\").unwrap_err();\n    }\n\n    // TODO: Test all attribute keywords (e.g. cpu, mem, etc.)\n    // #[test]\n    // fn test_all_attribute_keywords() {}\n\n    // TODO: Support 'bytes' or similar\n    // #[test]\n    // fn test_bytes_keyword() {\n    //     let mem = parse_query_no_options(\"mem > 50 bytes\").unwrap();\n    //     let mut process_a = simple_process(\"test\");\n    //     process_a.mem_usage = 100;\n\n    //     assert!(mem.check(&process_a, false));\n    // }\n}\n"
  },
  {
    "path": "src/widgets/process_table/sort_table.rs",
    "content": "use std::{borrow::Cow, num::NonZeroU16};\n\nuse crate::canvas::components::data_table::{ColumnHeader, DataTableColumn, DataToCell};\n\npub struct SortTableColumn;\n\nimpl ColumnHeader for SortTableColumn {\n    fn text(&self) -> Cow<'static, str> {\n        \"Sort By\".into()\n    }\n}\n\nimpl DataToCell<SortTableColumn> for &'static str {\n    fn to_cell_text(\n        &self, _column: &SortTableColumn, _calculated_width: NonZeroU16,\n    ) -> Option<Cow<'static, str>> {\n        Some(Cow::Borrowed(self))\n    }\n\n    fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>\n    where\n        Self: Sized,\n    {\n        vec![data.iter().map(|d| d.len() as u16).max().unwrap_or(0)]\n    }\n}\n\nimpl DataToCell<SortTableColumn> for Cow<'static, str> {\n    fn to_cell_text(\n        &self, _column: &SortTableColumn, _calculated_width: NonZeroU16,\n    ) -> Option<Cow<'static, str>> {\n        Some(self.clone())\n    }\n\n    fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>\n    where\n        Self: Sized,\n    {\n        vec![data.iter().map(|d| d.len() as u16).max().unwrap_or(0)]\n    }\n}\n"
  },
  {
    "path": "src/widgets/process_table.rs",
    "content": "pub mod process_columns;\npub mod process_data;\npub mod query;\nmod sort_table;\n\nuse std::{borrow::Cow, collections::BTreeMap};\n\nuse indexmap::IndexSet;\nuse itertools::Itertools;\nuse nohash::IntMap;\npub use process_columns::*;\npub use process_data::*;\nuse query::{ProcessQuery, parse_query};\nuse rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};\nuse sort_table::SortTableColumn;\n\nuse crate::{\n    app::{\n        AppConfigFields, AppSearchState,\n        data::{ProcessData, StoredData},\n    },\n    canvas::components::data_table::{\n        Column, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps,\n        DataTableStyling, SortColumn, SortDataTable, SortDataTableProps, SortOrder, SortsRow,\n    },\n    collection::processes::{Pid, ProcessHarvest},\n    options::config::style::Styles,\n    widgets::query::QueryOptions,\n};\n\n/// ProcessSearchState only deals with process' search's current settings and\n/// state.\n#[derive(Default)]\npub struct ProcessSearchState {\n    pub search_state: AppSearchState,\n    pub query_options: QueryOptions,\n}\n\nimpl ProcessSearchState {\n    pub fn search_toggle_ignore_case(&mut self) {\n        self.query_options.ignore_case = !self.query_options.ignore_case;\n    }\n\n    pub fn search_toggle_whole_word(&mut self) {\n        self.query_options.whole_word = !self.query_options.whole_word;\n    }\n\n    pub fn search_toggle_regex(&mut self) {\n        self.query_options.use_regex = !self.query_options.use_regex;\n    }\n}\n\n/// Whether to expand or collapse by default.\n#[derive(Clone, Debug, Eq, PartialEq)]\npub(crate) enum TreeCollapsed {\n    DefaultCollapse { expanded_pids: HashSet<Pid> },\n    DefaultExpand { collapsed_pids: HashSet<Pid> },\n}\n\nimpl TreeCollapsed {\n    /// Creates a new [`TreeCollapsed`].\n    pub(crate) fn new(default_collapsed: bool) -> Self {\n        if default_collapsed {\n            TreeCollapsed::DefaultCollapse {\n                expanded_pids: HashSet::default(),\n            }\n        } else {\n            TreeCollapsed::DefaultExpand {\n                collapsed_pids: HashSet::default(),\n            }\n        }\n    }\n\n    /// Check whether the given PID is collapsed.\n    pub(crate) fn is_collapsed(&self, pid: Pid) -> bool {\n        match self {\n            TreeCollapsed::DefaultCollapse { expanded_pids } => !expanded_pids.contains(&pid),\n            TreeCollapsed::DefaultExpand { collapsed_pids } => collapsed_pids.contains(&pid),\n        }\n    }\n\n    /// Collapse the given PID.\n    pub(crate) fn collapse(&mut self, pid: Pid) {\n        match self {\n            TreeCollapsed::DefaultCollapse { expanded_pids } => {\n                expanded_pids.remove(&pid);\n            }\n            TreeCollapsed::DefaultExpand { collapsed_pids } => {\n                collapsed_pids.insert(pid);\n            }\n        }\n    }\n\n    /// Expand the given PID.\n    pub(crate) fn expand(&mut self, pid: Pid) {\n        match self {\n            TreeCollapsed::DefaultCollapse { expanded_pids } => {\n                expanded_pids.insert(pid);\n            }\n            TreeCollapsed::DefaultExpand { collapsed_pids } => {\n                collapsed_pids.remove(&pid);\n            }\n        }\n    }\n\n    /// Toggle the given PID.\n    pub(crate) fn toggle(&mut self, pid: Pid) {\n        match self {\n            TreeCollapsed::DefaultCollapse { expanded_pids } => {\n                if expanded_pids.contains(&pid) {\n                    expanded_pids.remove(&pid);\n                } else {\n                    expanded_pids.insert(pid);\n                }\n            }\n            TreeCollapsed::DefaultExpand { collapsed_pids } => {\n                if collapsed_pids.contains(&pid) {\n                    collapsed_pids.remove(&pid);\n                } else {\n                    collapsed_pids.insert(pid);\n                }\n            }\n        }\n    }\n}\n\n#[derive(Clone, Debug, Eq, PartialEq)]\npub(crate) enum ProcWidgetMode {\n    Tree(TreeCollapsed),\n    Grouped,\n    Normal,\n}\n\ntype ProcessTable = SortDataTable<ProcWidgetData, ProcColumn>;\ntype SortTable = DataTable<Cow<'static, str>, SortTableColumn>;\ntype StringPidMap = HashMap<String, Vec<Pid>>;\n\nfn make_column(column: ProcColumn) -> SortColumn<ProcColumn> {\n    use ProcColumn::*;\n\n    match column {\n        CpuPercent => SortColumn::new(CpuPercent).default_descending(),\n        MemValue => SortColumn::new(MemValue).default_descending(),\n        MemPercent => SortColumn::new(MemPercent).default_descending(),\n        VirtualMem => SortColumn::new(VirtualMem).default_descending(),\n        Pid => SortColumn::new(Pid),\n        Count => SortColumn::new(Count),\n        Name => SortColumn::soft(Name, Some(0.3)),\n        Command => SortColumn::soft(Command, Some(0.3)),\n        ReadPerSecond => SortColumn::hard(ReadPerSecond, 8).default_descending(),\n        WritePerSecond => SortColumn::hard(WritePerSecond, 8).default_descending(),\n        TotalRead => SortColumn::hard(TotalRead, 8).default_descending(),\n        TotalWrite => SortColumn::hard(TotalWrite, 8).default_descending(),\n        User => SortColumn::soft(User, Some(0.05)),\n        State => SortColumn::hard(State, 9),\n        Time => SortColumn::new(Time),\n        Priority => SortColumn::new(Priority).default_descending(),\n        #[cfg(unix)]\n        Nice => SortColumn::new(Nice),\n        #[cfg(feature = \"gpu\")]\n        GpuMemValue => SortColumn::new(GpuMemValue).default_descending(),\n        #[cfg(feature = \"gpu\")]\n        GpuMemPercent => SortColumn::new(GpuMemPercent).default_descending(),\n        #[cfg(feature = \"gpu\")]\n        GpuUtilPercent => SortColumn::new(GpuUtilPercent).default_descending(),\n    }\n}\n\n#[derive(Clone, Copy, Default)]\npub struct ProcTableConfig {\n    pub is_case_sensitive: bool,\n    pub is_match_whole_word: bool,\n    pub is_use_regex: bool,\n    pub show_memory_as_values: bool,\n    pub is_command: bool,\n}\n\n/// A hacky workaround for now.\n#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]\npub enum ProcWidgetColumn {\n    PidOrCount,\n    ProcNameOrCommand,\n    Cpu,\n    Mem,\n    VirtualMem,\n    ReadPerSecond,\n    WritePerSecond,\n    TotalRead,\n    TotalWrite,\n    User,\n    State,\n    Time,\n    Priority,\n    #[cfg(unix)]\n    Nice,\n    #[cfg(feature = \"gpu\")]\n    GpuMem,\n    #[cfg(feature = \"gpu\")]\n    GpuUtil,\n}\n\n// This is temporary. Switch back to `ProcColumn` later!\n\npub struct ProcWidgetState {\n    pub(crate) mode: ProcWidgetMode,\n\n    /// The state of the search box.\n    pub proc_search: ProcessSearchState,\n\n    /// The state of the main table.\n    pub table: ProcessTable,\n\n    /// The state of the togglable table that controls sorting.\n    pub sort_table: SortTable,\n\n    /// The internal column mapping as an [`IndexSet`], to allow us to do quick\n    /// mappings of column type -> index.\n    pub column_mapping: IndexSet<ProcWidgetColumn>,\n\n    /// A name-to-pid mapping.\n    pub id_pid_map: StringPidMap,\n\n    /// The default sort index.\n    default_sort_index: usize,\n\n    /// The default sort order.\n    default_sort_order: SortOrder,\n\n    pub is_sort_open: bool,\n    pub force_rerender: bool,\n    pub force_update_data: bool,\n    #[cfg(target_os = \"linux\")]\n    pub hide_k_threads: bool,\n}\n\nimpl ProcWidgetState {\n    fn new_sort_table(config: &AppConfigFields, palette: &Styles) -> SortTable {\n        const COLUMNS: [Column<SortTableColumn>; 1] = [Column::hard(SortTableColumn, 7)];\n\n        let props = DataTableProps {\n            title: None,\n            table_gap: config.table_gap,\n            left_to_right: true,\n            is_basic: false,\n            show_table_scroll_position: false,\n            show_current_entry_when_unfocused: false,\n        };\n        let styling = DataTableStyling::from_palette(palette);\n\n        DataTable::new(COLUMNS, props, styling)\n    }\n\n    fn new_process_table(\n        config: &AppConfigFields, colours: &Styles, columns: Vec<SortColumn<ProcColumn>>,\n        default_index: usize, default_order: SortOrder,\n    ) -> ProcessTable {\n        let inner_props = DataTableProps {\n            title: Some(\" Processes \".into()),\n            table_gap: config.table_gap,\n            left_to_right: true,\n            is_basic: config.use_basic_mode,\n            show_table_scroll_position: config.show_table_scroll_position,\n            show_current_entry_when_unfocused: false,\n        };\n        let props = SortDataTableProps {\n            inner: inner_props,\n            sort_index: default_index,\n            order: default_order,\n        };\n        let styling = DataTableStyling::from_palette(colours);\n\n        DataTable::new_sortable(columns, props, styling)\n    }\n\n    pub(crate) fn new(\n        config: &AppConfigFields, mode: ProcWidgetMode, table_config: ProcTableConfig,\n        colours: &Styles, config_columns: &Option<IndexSet<ProcWidgetColumn>>,\n    ) -> Self {\n        let process_search_state = {\n            let mut pss = ProcessSearchState::default();\n\n            if table_config.is_case_sensitive {\n                // By default it's off.\n                pss.search_toggle_ignore_case();\n            }\n            if table_config.is_match_whole_word {\n                pss.search_toggle_whole_word();\n            }\n            if table_config.is_use_regex {\n                pss.search_toggle_regex();\n            }\n\n            pss\n        };\n\n        let columns: Vec<SortColumn<ProcColumn>> = {\n            use ProcColumn::*;\n\n            let is_count = matches!(mode, ProcWidgetMode::Grouped);\n            let is_command = table_config.is_command;\n            let mem_as_values = table_config.show_memory_as_values;\n\n            match config_columns {\n                Some(columns) if !columns.is_empty() => columns\n                    .into_iter()\n                    .map(|c| {\n                        let col = match c {\n                            ProcWidgetColumn::PidOrCount => {\n                                if is_count {\n                                    Count\n                                } else {\n                                    Pid\n                                }\n                            }\n                            ProcWidgetColumn::ProcNameOrCommand => {\n                                if is_command {\n                                    Command\n                                } else {\n                                    Name\n                                }\n                            }\n                            ProcWidgetColumn::Cpu => CpuPercent,\n                            ProcWidgetColumn::Mem => {\n                                if mem_as_values {\n                                    MemValue\n                                } else {\n                                    MemPercent\n                                }\n                            }\n                            ProcWidgetColumn::VirtualMem => VirtualMem,\n                            ProcWidgetColumn::ReadPerSecond => ReadPerSecond,\n                            ProcWidgetColumn::WritePerSecond => WritePerSecond,\n                            ProcWidgetColumn::TotalRead => TotalRead,\n                            ProcWidgetColumn::TotalWrite => TotalWrite,\n                            ProcWidgetColumn::User => User,\n                            ProcWidgetColumn::State => State,\n                            ProcWidgetColumn::Time => Time,\n                            ProcWidgetColumn::Priority => Priority,\n                            #[cfg(unix)]\n                            ProcWidgetColumn::Nice => Nice,\n                            #[cfg(feature = \"gpu\")]\n                            ProcWidgetColumn::GpuMem => {\n                                if mem_as_values {\n                                    GpuMemValue\n                                } else {\n                                    GpuMemPercent\n                                }\n                            }\n                            #[cfg(feature = \"gpu\")]\n                            ProcWidgetColumn::GpuUtil => GpuUtilPercent,\n                        };\n\n                        make_column(col)\n                    })\n                    .collect(),\n                _ => {\n                    let default_columns = [\n                        if is_count { Count } else { Pid },\n                        if is_command { Command } else { Name },\n                        CpuPercent,\n                        if mem_as_values { MemValue } else { MemPercent },\n                        ReadPerSecond,\n                        WritePerSecond,\n                        TotalRead,\n                        TotalWrite,\n                        User,\n                        State,\n                        Time,\n                        Priority,\n                        // Maybe add nice back as a default when I can figure out how to do the default configs better for Windows? As currently otherwise there's a mismatch.\n                    ];\n\n                    default_columns.into_iter().map(make_column).collect()\n                }\n            }\n        };\n\n        let column_mapping = columns\n            .iter()\n            .map(|col| {\n                use ProcColumn::*;\n\n                match col.inner() {\n                    CpuPercent => ProcWidgetColumn::Cpu,\n                    MemValue | MemPercent => ProcWidgetColumn::Mem,\n                    VirtualMem => ProcWidgetColumn::VirtualMem,\n                    Pid | Count => ProcWidgetColumn::PidOrCount,\n                    Name | Command => ProcWidgetColumn::ProcNameOrCommand,\n                    ReadPerSecond => ProcWidgetColumn::ReadPerSecond,\n                    WritePerSecond => ProcWidgetColumn::WritePerSecond,\n                    TotalRead => ProcWidgetColumn::TotalRead,\n                    TotalWrite => ProcWidgetColumn::TotalWrite,\n                    State => ProcWidgetColumn::State,\n                    User => ProcWidgetColumn::User,\n                    Time => ProcWidgetColumn::Time,\n                    Priority => ProcWidgetColumn::Priority,\n                    #[cfg(unix)]\n                    Nice => ProcWidgetColumn::Nice,\n                    #[cfg(feature = \"gpu\")]\n                    GpuMemValue | GpuMemPercent => ProcWidgetColumn::GpuMem,\n                    #[cfg(feature = \"gpu\")]\n                    GpuUtilPercent => ProcWidgetColumn::GpuUtil,\n                }\n            })\n            .collect::<IndexSet<_>>();\n\n        let (default_sort_index, default_sort_order) =\n            if matches!(mode, ProcWidgetMode::Tree { .. }) {\n                if let Some(index) = column_mapping.get_index_of(&ProcWidgetColumn::PidOrCount) {\n                    (index, columns[index].default_order)\n                } else {\n                    (0, columns[0].default_order)\n                }\n            } else if let Some(index) = column_mapping.get_index_of(&ProcWidgetColumn::Cpu) {\n                (index, columns[index].default_order)\n            } else {\n                (0, columns[0].default_order)\n            };\n\n        let sort_table = Self::new_sort_table(config, colours);\n        let table = Self::new_process_table(\n            config,\n            colours,\n            columns,\n            default_sort_index,\n            default_sort_order,\n        );\n\n        let id_pid_map = HashMap::default();\n\n        let mut table = ProcWidgetState {\n            proc_search: process_search_state,\n            table,\n            sort_table,\n            id_pid_map,\n            column_mapping,\n            is_sort_open: false,\n            mode,\n            force_rerender: true,\n            force_update_data: false,\n            default_sort_index,\n            default_sort_order,\n            #[cfg(target_os = \"linux\")]\n            hide_k_threads: config.hide_k_threads,\n        };\n        table.sort_table.set_data(table.column_text());\n\n        table\n    }\n\n    pub fn is_using_command(&self) -> bool {\n        self.column_mapping\n            .get_index_of(&ProcWidgetColumn::ProcNameOrCommand)\n            .and_then(|index| {\n                self.table\n                    .columns\n                    .get(index)\n                    .map(|col| matches!(col.inner(), ProcColumn::Command))\n            })\n            .unwrap_or(false)\n    }\n\n    pub fn is_mem_percent(&self) -> bool {\n        self.column_mapping\n            .get_index_of(&ProcWidgetColumn::Mem)\n            .and_then(|index| self.table.columns.get(index))\n            .map(|col| matches!(col.inner(), ProcColumn::MemPercent))\n            .unwrap_or(false)\n    }\n\n    fn get_query(&self) -> &Option<ProcessQuery> {\n        if self.proc_search.search_state.is_invalid_or_blank_search() {\n            &None\n        } else {\n            &self.proc_search.search_state.query\n        }\n    }\n\n    /// Update the current table data.\n    ///\n    /// This function *only* updates the displayed process data. If there is a\n    /// need to update the actual *stored* data, call it before this\n    /// function.\n    pub fn set_table_data(&mut self, stored_data: &StoredData) {\n        let data = match &self.mode {\n            ProcWidgetMode::Grouped | ProcWidgetMode::Normal => {\n                self.get_normal_data(&stored_data.process_data.process_harvest)\n            }\n            ProcWidgetMode::Tree(collapse) => self.get_tree_data(collapse, stored_data),\n        };\n        self.table.set_data(data);\n        self.force_update_data = false;\n    }\n\n    fn get_tree_data(\n        &self, collapsed: &TreeCollapsed, stored_data: &StoredData,\n    ) -> Vec<ProcWidgetData> {\n        const BRANCH_END: char = '└';\n        const BRANCH_SPLIT: char = '├';\n        const BRANCH_HORIZONTAL: char = '─';\n        const SPACED_BRANCH_VERTICAL: &str = \"│  \";\n\n        let search_query = self.get_query();\n        let is_using_command = self.is_using_command();\n        let is_mem_percent = self.is_mem_percent();\n\n        let ProcessData {\n            process_harvest,\n            process_parent_mapping,\n            orphan_pids,\n            ..\n        } = &stored_data.process_data;\n\n        // Only keep a set of the kept PIDs.\n        let kept_pids = stored_data\n            .process_data\n            .process_harvest\n            .iter()\n            .filter_map(|(pid, process)| {\n                if search_query\n                    .as_ref()\n                    .map(|q| q.check(process, is_using_command))\n                    .unwrap_or(true)\n                {\n                    #[cfg(target_os = \"linux\")]\n                    if self.hide_k_threads && process.process_type.is_kernel() {\n                        return None;\n                    }\n\n                    Some(*pid)\n                } else {\n                    None\n                }\n            })\n            .collect::<HashSet<_>>();\n\n        #[inline]\n        fn is_ancestor_shown(\n            current_process: &ProcessHarvest, kept_pids: &HashSet<Pid>,\n            process_harvest: &BTreeMap<Pid, ProcessHarvest>,\n        ) -> bool {\n            if let Some(ppid) = current_process.parent_pid {\n                if kept_pids.contains(&ppid) {\n                    true\n                } else if let Some(parent) = process_harvest.get(&ppid) {\n                    is_ancestor_shown(parent, kept_pids, process_harvest)\n                } else {\n                    false\n                }\n            } else {\n                false\n            }\n        }\n\n        // A process is shown under the filtered tree if at least one of these\n        // conditions hold:\n        // - The process itself matches.\n        // - The process contains some descendant that matches.\n        // - The process's parent (and only parent, not any ancestor) matches.\n        let filtered_tree = {\n            let mut filtered_tree: IntMap<Pid, Vec<Pid>> = IntMap::default();\n\n            // We do a simple DFS traversal to build our filtered parent-to-tree mappings.\n            let mut visited_pids: IntMap<Pid, bool> = IntMap::default();\n            let mut stack = orphan_pids\n                .iter()\n                .filter_map(|process| process_harvest.get(process))\n                .collect_vec();\n\n            while let Some(process) = stack.last() {\n                let is_process_matching = kept_pids.contains(&process.pid);\n\n                if let Some(children_pids) = process_parent_mapping.get(&process.pid) {\n                    if children_pids\n                        .iter()\n                        .all(|pid| visited_pids.contains_key(pid))\n                    {\n                        let shown_children = children_pids\n                            .iter()\n                            .filter(|pid| visited_pids.get(*pid).copied().unwrap_or(false))\n                            .collect_vec();\n\n                        // Show the entry if it is:\n                        // - Matches the filter.\n                        // - Has at least one child (doesn't have to be direct) that matches the\n                        //   filter.\n                        // - Is the child of a shown process.\n                        let is_shown = is_process_matching\n                            || !shown_children.is_empty()\n                            || is_ancestor_shown(process, &kept_pids, process_harvest);\n                        visited_pids.insert(process.pid, is_shown);\n\n                        if is_shown {\n                            filtered_tree.insert(\n                                process.pid,\n                                shown_children\n                                    .into_iter()\n                                    .filter_map(|pid| {\n                                        process_harvest.get(pid).map(|process| process.pid)\n                                    })\n                                    .collect_vec(),\n                            );\n                        }\n\n                        stack.pop();\n                    } else {\n                        children_pids\n                            .iter()\n                            .filter_map(|process| process_harvest.get(process))\n                            .rev()\n                            .for_each(|process| {\n                                stack.push(process);\n                            });\n                    }\n                } else {\n                    let is_shown = is_process_matching\n                        || is_ancestor_shown(process, &kept_pids, process_harvest);\n\n                    if is_shown {\n                        filtered_tree.insert(process.pid, vec![]);\n                    }\n\n                    visited_pids.insert(process.pid, is_shown);\n                    stack.pop();\n                }\n            }\n\n            filtered_tree\n        };\n\n        let mut data = vec![];\n        let mut prefixes = vec![];\n        let mut stack = orphan_pids\n            .iter()\n            .filter_map(|pid| {\n                if filtered_tree.contains_key(pid) {\n                    process_harvest.get(pid).map(|process| {\n                        ProcWidgetData::from_data(process, is_using_command, is_mem_percent)\n                    })\n                } else {\n                    None\n                }\n            })\n            .collect_vec();\n\n        stack.sort_unstable_by_key(|p| p.pid);\n\n        let column = self\n            .table\n            .columns\n            .get(self.table.sort_index())\n            .expect(\"columns should contain the current sort index\");\n        sort_skip_pid_asc(column.inner(), &mut stack, self.table.order());\n\n        let mut length_stack = vec![stack.len()];\n        stack.reverse();\n\n        while let (Some(process), Some(siblings_left)) = (stack.pop(), length_stack.last_mut()) {\n            *siblings_left -= 1;\n\n            let disabled = !kept_pids.contains(&process.pid);\n            let is_last = *siblings_left == 0;\n\n            if collapsed.is_collapsed(process.pid) {\n                let mut summed_process = process.clone();\n                let mut has_children = false;\n\n                if let Some(children_pids) = filtered_tree.get(&process.pid) {\n                    let mut sum_queue = children_pids\n                        .iter()\n                        .filter_map(|child| {\n                            process_harvest.get(child).map(|p| {\n                                ProcWidgetData::from_data(p, is_using_command, is_mem_percent)\n                            })\n                        })\n                        .collect_vec();\n\n                    while let Some(process) = sum_queue.pop() {\n                        summed_process.add(&process);\n\n                        if let Some(pids) = filtered_tree.get(&process.pid) {\n                            sum_queue.extend(pids.iter().filter_map(|child| {\n                                process_harvest.get(child).map(|p| {\n                                    ProcWidgetData::from_data(p, is_using_command, is_mem_percent)\n                                })\n                            }));\n                        }\n                    }\n\n                    has_children = !children_pids.is_empty();\n                }\n\n                // This is so that if an entry is \"collapsed\" but there are no children, avoid drawing the \"+\".\n                let prefix = if has_children {\n                    if prefixes.is_empty() {\n                        \"+ \".to_string()\n                    } else {\n                        format!(\n                            \"{}{}{} + \",\n                            prefixes.join(\"\"),\n                            if is_last { BRANCH_END } else { BRANCH_SPLIT },\n                            BRANCH_HORIZONTAL\n                        )\n                    }\n                } else if prefixes.is_empty() {\n                    String::default()\n                } else {\n                    format!(\n                        \"{}{}{} \",\n                        prefixes.join(\"\"),\n                        if is_last { BRANCH_END } else { BRANCH_SPLIT },\n                        BRANCH_HORIZONTAL\n                    )\n                };\n\n                data.push(summed_process.prefix(Some(prefix)).disabled(disabled));\n            } else {\n                let prefix = if prefixes.is_empty() {\n                    String::default()\n                } else {\n                    format!(\n                        \"{}{}{} \",\n                        prefixes.join(\"\"),\n                        if is_last { BRANCH_END } else { BRANCH_SPLIT },\n                        BRANCH_HORIZONTAL\n                    )\n                };\n                let pid = process.pid;\n                data.push(process.prefix(Some(prefix)).disabled(disabled));\n\n                if let Some(children_pids) = filtered_tree.get(&pid) {\n                    if prefixes.is_empty() {\n                        prefixes.push(\"\");\n                    } else {\n                        prefixes.push(if is_last {\n                            \"   \"\n                        } else {\n                            SPACED_BRANCH_VERTICAL\n                        });\n                    }\n\n                    let mut children = children_pids\n                        .iter()\n                        .filter_map(|child_pid| {\n                            process_harvest.get(child_pid).map(|p| {\n                                ProcWidgetData::from_data(p, is_using_command, is_mem_percent)\n                            })\n                        })\n                        .collect_vec();\n\n                    column.sort_by(&mut children, self.table.order().rev());\n\n                    length_stack.push(children.len());\n                    stack.extend(children);\n                }\n            }\n\n            while let Some(children_left) = length_stack.last() {\n                if *children_left == 0 {\n                    length_stack.pop();\n                    prefixes.pop();\n                } else {\n                    break;\n                }\n            }\n        }\n\n        data\n    }\n\n    fn get_normal_data(\n        &mut self, process_harvest: &BTreeMap<Pid, ProcessHarvest>,\n    ) -> Vec<ProcWidgetData> {\n        let search_query = self.get_query();\n        let is_using_command = self.is_using_command();\n        let is_mem_percent = self.is_mem_percent();\n\n        let filtered_iter = process_harvest.values().filter(|process| {\n            #[cfg(target_os = \"linux\")]\n            if self.hide_k_threads && process.process_type.is_kernel() {\n                return false;\n            }\n\n            search_query\n                .as_ref()\n                .map(|query| query.check(process, is_using_command))\n                .unwrap_or(true)\n        });\n\n        let mut id_pid_map: HashMap<String, Vec<Pid>> = HashMap::default();\n        let mut filtered_data: Vec<ProcWidgetData> = if let ProcWidgetMode::Grouped = self.mode {\n            let mut id_process_mapping: HashMap<&String, ProcWidgetData> = HashMap::default();\n\n            for process in filtered_iter {\n                let id = if is_using_command {\n                    &process.command\n                } else {\n                    &process.name\n                };\n                let pid = process.pid;\n\n                if let Some(entry) = id_pid_map.get_mut(id) {\n                    entry.push(pid);\n                } else {\n                    id_pid_map.insert(id.clone(), vec![pid]);\n                }\n\n                if let Some(pwd) = id_process_mapping.get_mut(id) {\n                    pwd.cpu_usage_percent += process.cpu_usage_percent;\n\n                    match &mut pwd.mem_usage {\n                        MemUsage::Percent(usage) => {\n                            *usage += process.mem_usage_percent;\n                        }\n                        MemUsage::Bytes(usage) => {\n                            *usage += process.mem_usage;\n                        }\n                    }\n\n                    pwd.rps += process.read_per_sec;\n                    pwd.wps += process.write_per_sec;\n                    pwd.total_read += process.total_read;\n                    pwd.total_write += process.total_write;\n                    pwd.time = pwd.time.max(process.time);\n                    #[cfg(feature = \"gpu\")]\n                    {\n                        pwd.gpu_usage += process.gpu_util;\n                        match &mut pwd.gpu_mem_usage {\n                            MemUsage::Percent(usage) => {\n                                *usage += process.gpu_mem_percent;\n                            }\n                            MemUsage::Bytes(usage) => {\n                                *usage += process.gpu_mem;\n                            }\n                        }\n                    }\n\n                    pwd.num_similar += 1;\n                } else {\n                    id_process_mapping.insert(\n                        id,\n                        ProcWidgetData::from_data(process, is_using_command, is_mem_percent),\n                    );\n                }\n            }\n\n            id_process_mapping.into_values().collect()\n        } else {\n            filtered_iter\n                .map(|process| ProcWidgetData::from_data(process, is_using_command, is_mem_percent))\n                .collect()\n        };\n\n        self.id_pid_map = id_pid_map;\n\n        if let Some(column) = self.table.columns.get(self.table.sort_index()) {\n            sort_skip_pid_asc(column.inner(), &mut filtered_data, self.table.order());\n        }\n\n        filtered_data\n    }\n\n    #[inline(always)]\n    fn get_mut_proc_col(&mut self, index: usize) -> Option<&mut ProcColumn> {\n        self.table.columns.get_mut(index).map(|col| col.inner_mut())\n    }\n\n    pub fn toggle_mem_percentage(&mut self) {\n        if let Some(index) = self.column_mapping.get_index_of(&ProcWidgetColumn::Mem) {\n            if let Some(mem) = self.get_mut_proc_col(index) {\n                match mem {\n                    ProcColumn::MemValue => {\n                        *mem = ProcColumn::MemPercent;\n                    }\n                    ProcColumn::MemPercent => {\n                        *mem = ProcColumn::MemValue;\n                    }\n                    _ => unreachable!(),\n                }\n\n                self.sort_table.set_data(self.column_text());\n                self.force_data_update();\n            }\n        }\n        #[cfg(feature = \"gpu\")]\n        if let Some(index) = self.column_mapping.get_index_of(&ProcWidgetColumn::GpuMem) {\n            if let Some(mem) = self.get_mut_proc_col(index) {\n                match mem {\n                    ProcColumn::GpuMemValue => {\n                        *mem = ProcColumn::GpuMemPercent;\n                    }\n                    ProcColumn::GpuMemPercent => {\n                        *mem = ProcColumn::GpuMemValue;\n                    }\n                    _ => unreachable!(),\n                }\n\n                self.sort_table.set_data(self.column_text());\n                self.force_data_update();\n            }\n        }\n    }\n\n    /// Forces an update of the data stored.\n    #[inline]\n    pub fn force_data_update(&mut self) {\n        self.force_update_data = true;\n    }\n\n    /// Forces an entire rerender and update of the data stored.\n    #[inline]\n    pub fn force_rerender_and_update(&mut self) {\n        self.force_rerender = true;\n        self.force_update_data = true;\n    }\n\n    #[cfg(target_os = \"linux\")]\n    pub fn toggle_k_thread(&mut self) {\n        self.hide_k_threads = !self.hide_k_threads;\n        self.force_rerender_and_update();\n    }\n\n    /// Marks the selected column as hidden, and automatically resets the\n    /// selected column to the default sort index and order.\n    fn hide_column(&mut self, column: ProcWidgetColumn) {\n        if let Some(index) = self.column_mapping.get_index_of(&column) {\n            if let Some(col) = self.table.columns.get_mut(index) {\n                col.is_hidden = true;\n\n                if self.table.sort_index() == index {\n                    self.table.set_sort_index(self.default_sort_index);\n                    self.table.set_order(self.default_sort_order);\n                }\n            }\n        }\n    }\n\n    /// Marks the selected column as shown.\n    fn show_column(&mut self, column: ProcWidgetColumn) {\n        if let Some(index) = self.column_mapping.get_index_of(&column) {\n            if let Some(col) = self.table.columns.get_mut(index) {\n                col.is_hidden = false;\n            }\n        }\n    }\n\n    /// Select a column. If the column is already selected, then just toggle the\n    /// sort order.\n    pub fn select_column(&mut self, column: ProcWidgetColumn) {\n        if let Some(index) = self.column_mapping.get_index_of(&column) {\n            self.table.set_sort_index(index);\n            self.force_data_update();\n        }\n    }\n\n    pub fn collapse_current_tree_branch_entry(&mut self) {\n        if let ProcWidgetMode::Tree(collapsed) = &mut self.mode {\n            if let Some(process) = self.table.current_item() {\n                collapsed.collapse(process.pid);\n                self.force_data_update();\n            }\n        }\n    }\n\n    pub fn expand_current_tree_branch_entry(&mut self) {\n        if let ProcWidgetMode::Tree(collapsed) = &mut self.mode {\n            if let Some(process) = self.table.current_item() {\n                collapsed.expand(process.pid);\n                self.force_data_update();\n            }\n        }\n    }\n\n    pub fn toggle_current_tree_branch_entry(&mut self) {\n        if let ProcWidgetMode::Tree(collapsed) = &mut self.mode {\n            if let Some(process) = self.table.current_item() {\n                collapsed.toggle(process.pid);\n                self.force_data_update();\n            }\n        }\n    }\n\n    pub fn toggle_command(&mut self) {\n        if let Some(index) = self\n            .column_mapping\n            .get_index_of(&ProcWidgetColumn::ProcNameOrCommand)\n        {\n            if let Some(col) = self.table.columns.get_mut(index) {\n                let inner = col.inner_mut();\n                match inner {\n                    ProcColumn::Name => {\n                        *inner = ProcColumn::Command;\n                        if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() {\n                            *max_percentage = Some(0.5);\n                        }\n                    }\n                    ProcColumn::Command => {\n                        *inner = ProcColumn::Name;\n                        if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() {\n                            *max_percentage = match self.mode {\n                                ProcWidgetMode::Tree { .. } => Some(0.5),\n                                ProcWidgetMode::Grouped | ProcWidgetMode::Normal => Some(0.3),\n                            };\n                        }\n                    }\n                    _ => unreachable!(),\n                }\n                self.sort_table.set_data(self.column_text());\n                self.force_rerender_and_update();\n            }\n        }\n    }\n\n    /// Toggles the appropriate columns/settings when tab is pressed.\n    ///\n    /// If count is enabled, we should set the mode to\n    /// [`ProcWidgetMode::Grouped`], and switch off the User and State\n    /// columns. We should also move the user off of the columns if they were\n    /// selected, as those columns are now hidden (handled by internal\n    /// method calls), and go back to the \"defaults\".\n    ///\n    /// Otherwise, if count is disabled, then if the columns exist, the User and\n    /// State columns should be re-enabled, and the mode switched to\n    /// [`ProcWidgetMode::Normal`].\n    pub fn toggle_tab(&mut self) {\n        if !matches!(self.mode, ProcWidgetMode::Tree { .. }) {\n            if let Some(index) = self\n                .column_mapping\n                .get_index_of(&ProcWidgetColumn::PidOrCount)\n            {\n                if let Some(sort_col) = self.table.columns.get_mut(index) {\n                    let col = sort_col.inner_mut();\n                    match col {\n                        ProcColumn::Pid => {\n                            *col = ProcColumn::Count;\n                            sort_col.default_order = SortOrder::Descending;\n\n                            self.hide_column(ProcWidgetColumn::User);\n                            self.hide_column(ProcWidgetColumn::State);\n                            self.mode = ProcWidgetMode::Grouped;\n                        }\n                        ProcColumn::Count => {\n                            *col = ProcColumn::Pid;\n                            sort_col.default_order = SortOrder::Ascending;\n\n                            self.show_column(ProcWidgetColumn::User);\n                            self.show_column(ProcWidgetColumn::State);\n                            self.mode = ProcWidgetMode::Normal;\n                        }\n                        _ => unreachable!(),\n                    }\n\n                    self.sort_table.set_data(self.column_text());\n                    self.force_rerender_and_update();\n                }\n            }\n        }\n    }\n\n    pub fn column_text(&self) -> Vec<Cow<'static, str>> {\n        self.table\n            .columns\n            .iter()\n            .filter(|c| !c.is_hidden)\n            .map(|c| c.inner().text())\n            .collect::<Vec<_>>()\n    }\n\n    pub fn cursor_char_index(&self) -> usize {\n        self.proc_search.search_state.grapheme_cursor.cur_cursor()\n    }\n\n    pub fn is_search_enabled(&self) -> bool {\n        self.proc_search.search_state.is_enabled\n    }\n\n    pub fn current_search_query(&self) -> &str {\n        &self.proc_search.search_state.current_search_query\n    }\n\n    /// Update the current search query.\n    ///\n    /// TODO: Maybe debounce this.\n    pub fn update_query(&mut self) {\n        if self\n            .proc_search\n            .search_state\n            .current_search_query\n            .is_empty()\n        {\n            self.proc_search.search_state.is_blank_search = true;\n            self.proc_search.search_state.is_invalid_search = false;\n            self.proc_search.search_state.error_message = None;\n        } else {\n            match parse_query(\n                &self.proc_search.search_state.current_search_query,\n                &self.proc_search.query_options,\n            ) {\n                Ok(parsed_query) => {\n                    self.proc_search.search_state.query = Some(parsed_query);\n                    self.proc_search.search_state.is_blank_search = false;\n                    self.proc_search.search_state.is_invalid_search = false;\n                    self.proc_search.search_state.error_message = None;\n                }\n                Err(err) => {\n                    self.proc_search.search_state.is_blank_search = false;\n                    self.proc_search.search_state.is_invalid_search = true;\n                    self.proc_search.search_state.error_message = Some(err.to_string());\n                }\n            }\n        }\n        self.table.state.display_start_index = 0;\n        self.table.state.current_index = 0;\n\n        // Update the internal sizes too.\n        self.proc_search.search_state.update_sizes();\n\n        self.force_data_update();\n    }\n\n    pub fn clear_search(&mut self) {\n        self.proc_search.search_state.reset();\n        self.force_data_update();\n    }\n\n    pub fn search_walk_forward(&mut self) {\n        self.proc_search.search_state.walk_forward();\n    }\n\n    pub fn search_walk_back(&mut self) {\n        self.proc_search.search_state.walk_backward();\n    }\n\n    /// Sets the [`ProcWidgetState`]'s current sort index to whatever was in the\n    /// sort table if possible, then closes the sort table.\n    pub(crate) fn use_sort_table_value(&mut self) {\n        self.table.set_sort_index(self.sort_table.current_index());\n\n        self.is_sort_open = false;\n        self.force_rerender_and_update();\n    }\n\n    #[cfg(test)]\n    pub(crate) fn test_equality(&self, other: &Self) -> bool {\n        self.mode == other.mode\n            && self.proc_search.query_options == other.proc_search.query_options\n            && self\n                .table\n                .columns\n                .iter()\n                .map(|c| c.header())\n                .collect::<Vec<_>>()\n                == other\n                    .table\n                    .columns\n                    .iter()\n                    .map(|c| c.header())\n                    .collect::<Vec<_>>()\n    }\n}\n\n#[inline]\nfn sort_skip_pid_asc(column: &ProcColumn, data: &mut [ProcWidgetData], order: SortOrder) {\n    let descending = matches!(order, SortOrder::Descending);\n    match column {\n        ProcColumn::Pid if !descending => {}\n        _ => {\n            column.sort_data(data, descending);\n        }\n    }\n}\n\n#[cfg(test)]\nmod test {\n    use std::time::Duration;\n\n    use super::*;\n    #[cfg(target_os = \"linux\")]\n    use crate::collection::processes::ProcessType;\n    use crate::widgets::MemUsage;\n\n    #[test]\n    fn test_proc_sort() {\n        let a = ProcWidgetData {\n            pid: 1,\n            ppid: None,\n            id: \"A\".into(),\n            cpu_usage_percent: 0.0,\n            mem_usage: MemUsage::Percent(1.1),\n            virtual_mem: 100,\n            rps: 0,\n            wps: 0,\n            total_read: 0,\n            total_write: 0,\n            process_state: \"N/A\",\n            process_char: '?',\n            #[cfg(unix)]\n            user: Some(\"root\".into()),\n            #[cfg(not(target_family = \"unix\"))]\n            user: Some(\"N/A\".into()),\n            num_similar: 0,\n            disabled: false,\n            time: Duration::from_secs(0),\n            #[cfg(feature = \"gpu\")]\n            gpu_mem_usage: MemUsage::Percent(1.1),\n            #[cfg(feature = \"gpu\")]\n            gpu_usage: 0,\n            #[cfg(target_os = \"linux\")]\n            process_type: crate::collection::processes::ProcessType::Regular,\n            #[cfg(unix)]\n            nice: 0,\n            priority: -20,\n        };\n\n        let b = ProcWidgetData {\n            pid: 2,\n            ppid: Some(1),\n            id: \"B\".into(),\n            cpu_usage_percent: 1.1,\n            mem_usage: MemUsage::Percent(2.2),\n            ..(a.clone())\n        };\n\n        let c = ProcWidgetData {\n            pid: 3,\n            ppid: Some(1),\n            id: \"C\".into(),\n            cpu_usage_percent: 2.2,\n            mem_usage: MemUsage::Percent(0.0),\n            ..(a.clone())\n        };\n\n        let d = ProcWidgetData {\n            pid: 4,\n            ppid: Some(2),\n            id: \"D\".into(),\n            cpu_usage_percent: 0.0,\n            mem_usage: MemUsage::Percent(0.0),\n            ..(a.clone())\n        };\n        let mut data = vec![d.clone(), b.clone(), c.clone(), a.clone()];\n\n        // Assume we had sorted over by pid.\n        data.sort_by_key(|p| p.pid);\n        sort_skip_pid_asc(&ProcColumn::CpuPercent, &mut data, SortOrder::Descending);\n        assert_eq!(\n            [&c, &b, &a, &d].iter().map(|d| d.pid).collect::<Vec<_>>(),\n            data.iter().map(|d| d.pid).collect::<Vec<_>>(),\n        );\n\n        // Note that the PID ordering for ties is still ascending.\n        data.sort_by_key(|p| p.pid);\n        sort_skip_pid_asc(&ProcColumn::CpuPercent, &mut data, SortOrder::Ascending);\n        assert_eq!(\n            [&a, &d, &b, &c].iter().map(|d| d.pid).collect::<Vec<_>>(),\n            data.iter().map(|d| d.pid).collect::<Vec<_>>(),\n        );\n\n        data.sort_by_key(|p| p.pid);\n        sort_skip_pid_asc(&ProcColumn::MemPercent, &mut data, SortOrder::Descending);\n        assert_eq!(\n            [&b, &a, &c, &d].iter().map(|d| d.pid).collect::<Vec<_>>(),\n            data.iter().map(|d| d.pid).collect::<Vec<_>>(),\n        );\n\n        // Note that the PID ordering for ties is still ascending.\n        data.sort_by_key(|p| p.pid);\n        sort_skip_pid_asc(&ProcColumn::MemPercent, &mut data, SortOrder::Ascending);\n        assert_eq!(\n            [&c, &d, &a, &b].iter().map(|d| d.pid).collect::<Vec<_>>(),\n            data.iter().map(|d| d.pid).collect::<Vec<_>>(),\n        );\n    }\n\n    fn get_columns(table: &ProcessTable) -> Vec<ProcColumn> {\n        table\n            .columns\n            .iter()\n            .filter_map(|c| {\n                if c.is_hidden() {\n                    None\n                } else {\n                    Some(*c.inner())\n                }\n            })\n            .collect::<Vec<_>>()\n    }\n\n    fn init_state(table_config: ProcTableConfig, columns: &[ProcWidgetColumn]) -> ProcWidgetState {\n        let config = AppConfigFields::default();\n        let styling = Styles::default();\n        let columns = Some(columns.iter().cloned().collect());\n\n        ProcWidgetState::new(\n            &config,\n            ProcWidgetMode::Normal,\n            table_config,\n            &styling,\n            &columns,\n        )\n    }\n\n    fn init_default_state(columns: &[ProcWidgetColumn]) -> ProcWidgetState {\n        init_state(ProcTableConfig::default(), columns)\n    }\n\n    #[test]\n    fn custom_columns() {\n        let init_columns = vec![\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::ProcNameOrCommand,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::State,\n        ];\n        let columns = vec![\n            ProcColumn::Pid,\n            ProcColumn::Name,\n            ProcColumn::MemPercent,\n            ProcColumn::State,\n        ];\n        let state = init_default_state(&init_columns);\n        assert_eq!(get_columns(&state.table), columns);\n    }\n\n    #[test]\n    fn toggle_count_pid() {\n        let init_columns = [\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::ProcNameOrCommand,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::State,\n        ];\n        let original_columns = vec![\n            ProcColumn::Pid,\n            ProcColumn::Name,\n            ProcColumn::MemPercent,\n            ProcColumn::State,\n        ];\n        let new_columns = vec![ProcColumn::Count, ProcColumn::Name, ProcColumn::MemPercent];\n\n        let mut state = init_default_state(&init_columns);\n        assert_eq!(get_columns(&state.table), original_columns);\n\n        // This should hide the state.\n        state.toggle_tab();\n        assert_eq!(get_columns(&state.table), new_columns);\n\n        // This should re-reveal the state.\n        state.toggle_tab();\n        assert_eq!(get_columns(&state.table), original_columns);\n    }\n\n    #[test]\n    fn toggle_count_pid_2() {\n        let init_columns = [\n            ProcWidgetColumn::ProcNameOrCommand,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::User,\n            ProcWidgetColumn::State,\n            ProcWidgetColumn::PidOrCount,\n        ];\n        let original_columns = vec![\n            ProcColumn::Name,\n            ProcColumn::MemPercent,\n            ProcColumn::User,\n            ProcColumn::State,\n            ProcColumn::Pid,\n        ];\n        let new_columns = vec![ProcColumn::Name, ProcColumn::MemPercent, ProcColumn::Count];\n\n        let mut state = init_default_state(&init_columns);\n        assert_eq!(get_columns(&state.table), original_columns);\n\n        // This should hide the state.\n        state.toggle_tab();\n        assert_eq!(get_columns(&state.table), new_columns);\n\n        // This should re-reveal the state.\n        state.toggle_tab();\n        assert_eq!(get_columns(&state.table), original_columns);\n    }\n\n    #[test]\n    fn toggle_command() {\n        let init_columns = [\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::State,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::ProcNameOrCommand,\n        ];\n        let original_columns = vec![\n            ProcColumn::Pid,\n            ProcColumn::State,\n            ProcColumn::MemPercent,\n            ProcColumn::Command,\n        ];\n        let new_columns = vec![\n            ProcColumn::Pid,\n            ProcColumn::State,\n            ProcColumn::MemPercent,\n            ProcColumn::Name,\n        ];\n\n        let table_config = ProcTableConfig {\n            is_command: true,\n            ..Default::default()\n        };\n        let mut state = init_state(table_config, &init_columns);\n        assert_eq!(get_columns(&state.table), original_columns);\n\n        state.toggle_command();\n        assert_eq!(get_columns(&state.table), new_columns);\n\n        state.toggle_command();\n        assert_eq!(get_columns(&state.table), original_columns);\n    }\n\n    #[test]\n    fn toggle_mem_percentage() {\n        let init_columns = [\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::State,\n            ProcWidgetColumn::ProcNameOrCommand,\n        ];\n        let original_columns = vec![\n            ProcColumn::Pid,\n            ProcColumn::MemPercent,\n            ProcColumn::State,\n            ProcColumn::Name,\n        ];\n        let new_columns = vec![\n            ProcColumn::Pid,\n            ProcColumn::MemValue,\n            ProcColumn::State,\n            ProcColumn::Name,\n        ];\n\n        let mut state = init_default_state(&init_columns);\n        assert_eq!(get_columns(&state.table), original_columns);\n\n        state.toggle_mem_percentage();\n        assert_eq!(get_columns(&state.table), new_columns);\n\n        state.toggle_mem_percentage();\n        assert_eq!(get_columns(&state.table), original_columns);\n    }\n\n    #[test]\n    fn toggle_mem_percentage_2() {\n        let init_columns = [\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::State,\n            ProcWidgetColumn::ProcNameOrCommand,\n        ];\n        let original_columns = vec![\n            ProcColumn::Pid,\n            ProcColumn::MemValue,\n            ProcColumn::State,\n            ProcColumn::Name,\n        ];\n        let new_columns = vec![\n            ProcColumn::Pid,\n            ProcColumn::MemPercent,\n            ProcColumn::State,\n            ProcColumn::Name,\n        ];\n\n        let table_config = ProcTableConfig {\n            show_memory_as_values: true,\n            ..Default::default()\n        };\n        let mut state = init_state(table_config, &init_columns);\n        assert_eq!(get_columns(&state.table), original_columns);\n\n        state.toggle_mem_percentage();\n        assert_eq!(get_columns(&state.table), new_columns);\n\n        state.toggle_mem_percentage();\n        assert_eq!(get_columns(&state.table), original_columns);\n    }\n\n    #[test]\n    fn columns_and_is_using_command() {\n        let init_columns = [\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::State,\n            ProcWidgetColumn::ProcNameOrCommand,\n        ];\n        let original_columns = vec![\n            ProcColumn::Pid,\n            ProcColumn::MemPercent,\n            ProcColumn::State,\n            ProcColumn::Command,\n        ];\n\n        let table_config = ProcTableConfig {\n            is_command: true,\n            ..Default::default()\n        };\n        let mut state = init_state(table_config, &init_columns);\n        assert_eq!(get_columns(&state.table), original_columns);\n        assert!(state.is_using_command());\n\n        state.toggle_command();\n        assert!(!state.is_using_command());\n\n        state.toggle_command();\n        assert!(state.is_using_command());\n    }\n\n    #[test]\n    fn columns_and_is_memory() {\n        let init_columns = [\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::State,\n            ProcWidgetColumn::ProcNameOrCommand,\n        ];\n        let original_columns = vec![\n            ProcColumn::Pid,\n            ProcColumn::MemValue,\n            ProcColumn::State,\n            ProcColumn::Name,\n        ];\n\n        let table_config = ProcTableConfig {\n            show_memory_as_values: true,\n            ..Default::default()\n        };\n        let mut state = init_state(table_config, &init_columns);\n        assert_eq!(get_columns(&state.table), original_columns);\n        assert!(!state.is_mem_percent());\n\n        state.toggle_mem_percentage();\n        assert!(state.is_mem_percent());\n\n        state.toggle_mem_percentage();\n        assert!(!state.is_mem_percent());\n    }\n\n    /// Tests toggling if both mem and mem% columns are configured.\n    ///\n    /// Currently, this test doesn't really do much, since we treat these two\n    /// columns as the same - this test is intended for use later when we\n    /// might allow both at the same time.\n    #[test]\n    fn double_memory_sim_toggle() {\n        let init_columns = [\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::State,\n            ProcWidgetColumn::ProcNameOrCommand,\n            ProcWidgetColumn::Mem,\n        ];\n        let original_columns = vec![\n            ProcColumn::MemPercent,\n            ProcColumn::Pid,\n            ProcColumn::State,\n            ProcColumn::Name,\n        ];\n        let new_columns = vec![\n            ProcColumn::MemValue,\n            ProcColumn::Pid,\n            ProcColumn::State,\n            ProcColumn::Name,\n        ];\n\n        let mut state = init_default_state(&init_columns);\n        assert_eq!(get_columns(&state.table), original_columns);\n\n        state.toggle_mem_percentage();\n        assert_eq!(get_columns(&state.table), new_columns);\n\n        state.toggle_mem_percentage();\n        assert_eq!(get_columns(&state.table), original_columns);\n    }\n\n    /// Tests toggling if both pid and count columns are configured.\n    ///\n    /// Currently, this test doesn't really do much, since we treat these two\n    /// columns as the same - this test is intended for use later when we\n    /// might allow both at the same time.\n    #[test]\n    fn pid_and_count_sim_toggle() {\n        let init_columns = [\n            ProcWidgetColumn::ProcNameOrCommand,\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::State,\n            ProcWidgetColumn::PidOrCount,\n        ];\n        let original_columns = vec![\n            ProcColumn::Name,\n            ProcColumn::Pid,\n            ProcColumn::MemPercent,\n            ProcColumn::State,\n        ];\n        let new_columns = vec![ProcColumn::Name, ProcColumn::Count, ProcColumn::MemPercent];\n\n        let mut state = init_default_state(&init_columns);\n        assert_eq!(get_columns(&state.table), original_columns);\n\n        // This should hide the state.\n        state.toggle_tab();\n        assert_eq!(get_columns(&state.table), new_columns);\n\n        // This should re-reveal the state.\n        state.toggle_tab();\n        assert_eq!(get_columns(&state.table), original_columns);\n    }\n\n    /// Tests toggling if both command and name columns are configured.\n    ///\n    /// Currently, this test doesn't really do much, since we treat these two\n    /// columns as the same - this test is intended for use later when we\n    /// might allow both at the same time.\n    #[test]\n    fn command_name_sim_toggle() {\n        let init_columns = [\n            ProcWidgetColumn::ProcNameOrCommand,\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::State,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::ProcNameOrCommand,\n        ];\n        let original_columns = vec![\n            ProcColumn::Command,\n            ProcColumn::Pid,\n            ProcColumn::State,\n            ProcColumn::MemPercent,\n        ];\n        let new_columns = vec![\n            ProcColumn::Name,\n            ProcColumn::Pid,\n            ProcColumn::State,\n            ProcColumn::MemPercent,\n        ];\n\n        let table_config = ProcTableConfig {\n            is_command: true,\n            ..Default::default()\n        };\n        let mut state = init_state(table_config, &init_columns);\n        assert_eq!(get_columns(&state.table), original_columns);\n\n        state.toggle_command();\n        assert_eq!(get_columns(&state.table), new_columns);\n\n        state.toggle_command();\n        assert_eq!(get_columns(&state.table), original_columns);\n    }\n\n    /// Sanity test to ensure tree collapse logic works, both when enabled-by-default or disabled-by-default.\n    #[test]\n    fn test_tree_collapse() {\n        {\n            let mut collapsed_by_default = TreeCollapsed::new(true);\n\n            assert!(collapsed_by_default.is_collapsed(1));\n\n            collapsed_by_default.collapse(1);\n            assert!(collapsed_by_default.is_collapsed(1));\n\n            collapsed_by_default.expand(1);\n            assert!(!collapsed_by_default.is_collapsed(1));\n\n            collapsed_by_default.toggle(1);\n            assert!(collapsed_by_default.is_collapsed(1));\n\n            collapsed_by_default.toggle(1);\n            assert!(!collapsed_by_default.is_collapsed(1));\n        }\n\n        {\n            let mut expanded_by_default = TreeCollapsed::new(false);\n\n            assert!(!expanded_by_default.is_collapsed(1));\n\n            expanded_by_default.collapse(1);\n            assert!(expanded_by_default.is_collapsed(1));\n\n            expanded_by_default.expand(1);\n            assert!(!expanded_by_default.is_collapsed(1));\n\n            expanded_by_default.toggle(1);\n            assert!(expanded_by_default.is_collapsed(1));\n\n            expanded_by_default.toggle(1);\n            assert!(!expanded_by_default.is_collapsed(1));\n        }\n    }\n    #[cfg(target_os = \"linux\")]\n    /// Sanity test to ensure kernel thread processes are toggled\n    #[test]\n    fn test_toggle_k_threads() {\n        let init_columns = [\n            ProcWidgetColumn::ProcNameOrCommand,\n            ProcWidgetColumn::PidOrCount,\n            ProcWidgetColumn::State,\n            ProcWidgetColumn::Mem,\n            ProcWidgetColumn::ProcNameOrCommand,\n        ];\n        let mut state = init_default_state(&init_columns);\n        let process_harvest = ProcessHarvest {\n            pid: 1,\n            ..Default::default()\n        };\n        let k_process_harvest = ProcessHarvest {\n            pid: 2,\n            process_type: ProcessType::Kernel,\n            ..Default::default()\n        };\n        // test get_normal_data default is filtered by toggle_k_thread\n        let mut normal_proc_harvest: BTreeMap<Pid, ProcessHarvest> = BTreeMap::new();\n        normal_proc_harvest.insert(1, process_harvest.clone());\n        normal_proc_harvest.insert(2, k_process_harvest.clone());\n        let default_normal_results = state.get_normal_data(&normal_proc_harvest).len();\n        assert!(default_normal_results == 2);\n        state.toggle_k_thread();\n        let filtered_normal_results = state.get_normal_data(&normal_proc_harvest).len();\n        assert!(filtered_normal_results == 1);\n        // test that get_normal_data in grouped mode is still filtered\n        state.mode = ProcWidgetMode::Grouped;\n        let filtered_grouped_results = state.get_normal_data(&normal_proc_harvest).len();\n        assert!(filtered_grouped_results == 1);\n        // test that get_tree_data is filtered on toggle_k_thread\n        let tree_collapsed = TreeCollapsed::new(false);\n        state.mode = ProcWidgetMode::Tree(tree_collapsed.clone());\n        state.hide_k_threads = false;\n        let mut tree_proc_data = ProcessData::default();\n        tree_proc_data.process_harvest.insert(1, process_harvest);\n        tree_proc_data.process_harvest.insert(2, k_process_harvest);\n        tree_proc_data.orphan_pids = vec![1, 2];\n        let tree_stored_data = StoredData {\n            process_data: tree_proc_data,\n            ..Default::default()\n        };\n        let default_tree_results = state\n            .get_tree_data(&tree_collapsed, &tree_stored_data)\n            .len();\n        assert!(default_tree_results == 2);\n        state.toggle_k_thread();\n        let filtered_tree_results = state\n            .get_tree_data(&tree_collapsed, &tree_stored_data)\n            .len();\n        assert!(filtered_tree_results == 1);\n    }\n}\n"
  },
  {
    "path": "src/widgets/temperature_table.rs",
    "content": "use std::{borrow::Cow, cmp::max, num::NonZeroU16};\n\nuse crate::{\n    app::{AppConfigFields, data::TypedTemperature},\n    canvas::components::data_table::{\n        ColumnHeader, DataTableColumn, DataTableProps, DataTableStyling, DataToCell, SortColumn,\n        SortDataTable, SortDataTableProps, SortOrder, SortsRow,\n    },\n    options::config::style::Styles,\n    utils::general::sort_partial_fn,\n};\n\n#[derive(Clone, Debug)]\npub struct TempWidgetData {\n    pub sensor: String,\n    pub temperature: Option<TypedTemperature>,\n}\n\npub enum TempWidgetColumn {\n    Sensor,\n    Temp,\n}\n\nimpl ColumnHeader for TempWidgetColumn {\n    fn text(&self) -> Cow<'static, str> {\n        match self {\n            TempWidgetColumn::Sensor => \"Sensor(s)\".into(),\n            TempWidgetColumn::Temp => \"Temp(t)\".into(),\n        }\n    }\n}\n\nimpl TempWidgetData {\n    pub fn temperature(&self) -> Cow<'static, str> {\n        match &self.temperature {\n            Some(temp) => temp.to_string().into(),\n            None => \"N/A\".into(),\n        }\n    }\n}\n\nimpl DataToCell<TempWidgetColumn> for TempWidgetData {\n    fn to_cell_text(\n        &self, column: &TempWidgetColumn, _calculated_width: NonZeroU16,\n    ) -> Option<Cow<'static, str>> {\n        Some(match column {\n            TempWidgetColumn::Sensor => self.sensor.clone().into(),\n            TempWidgetColumn::Temp => self.temperature(),\n        })\n    }\n\n    fn column_widths<C: DataTableColumn<TempWidgetColumn>>(\n        data: &[TempWidgetData], _columns: &[C],\n    ) -> Vec<u16>\n    where\n        Self: Sized,\n    {\n        let mut widths = vec![0; 2];\n\n        data.iter().for_each(|row| {\n            widths[0] = max(widths[0], row.sensor.len() as u16);\n            widths[1] = max(widths[1], row.temperature().len() as u16);\n        });\n\n        widths\n    }\n}\n\nimpl SortsRow for TempWidgetColumn {\n    type DataType = TempWidgetData;\n\n    fn sort_data(&self, data: &mut [Self::DataType], descending: bool) {\n        match self {\n            TempWidgetColumn::Sensor => {\n                data.sort_by(move |a, b| sort_partial_fn(descending)(&a.sensor, &b.sensor));\n            }\n            TempWidgetColumn::Temp => {\n                data.sort_by(|a, b| sort_partial_fn(descending)(&a.temperature, &b.temperature));\n            }\n        }\n    }\n}\n\npub struct TempWidgetState {\n    pub table: SortDataTable<TempWidgetData, TempWidgetColumn>,\n    pub force_update_data: bool,\n}\n\nimpl TempWidgetState {\n    pub(crate) fn new(config: &AppConfigFields, palette: &Styles) -> Self {\n        let columns = [\n            SortColumn::soft(TempWidgetColumn::Sensor, Some(0.8)),\n            SortColumn::soft(TempWidgetColumn::Temp, None).default_descending(),\n        ];\n\n        let props = SortDataTableProps {\n            inner: DataTableProps {\n                title: Some(\" Temperatures \".into()),\n                table_gap: config.table_gap,\n                left_to_right: false,\n                is_basic: config.use_basic_mode,\n                show_table_scroll_position: config.show_table_scroll_position,\n                show_current_entry_when_unfocused: false,\n            },\n            sort_index: 0,\n            order: SortOrder::Ascending,\n        };\n\n        let styling = DataTableStyling::from_palette(palette);\n\n        Self {\n            table: SortDataTable::new_sortable(columns, props, styling),\n            force_update_data: false,\n        }\n    }\n\n    /// Forces an update of the data stored.\n    #[inline]\n    pub fn force_data_update(&mut self) {\n        self.force_update_data = true;\n    }\n\n    /// Update the current table data.\n    pub fn set_table_data(&mut self, data: &[TempWidgetData]) {\n        let mut data = data.to_vec();\n        if let Some(column) = self.table.columns.get(self.table.sort_index()) {\n            column.sort_by(&mut data, self.table.order());\n        }\n        self.table.set_data(data);\n        self.force_update_data = false;\n    }\n}\n"
  },
  {
    "path": "tests/integration/arg_tests.rs",
    "content": "//! These tests are mostly here just to ensure that invalid results will be\n//! caught when passing arguments.\n\nuse assert_cmd::prelude::*;\nuse predicates::prelude::*;\n\nuse crate::util::{btm_command, no_cfg_btm_command};\n\n#[test]\nfn test_small_rate() {\n    btm_command(&[\"-C\", \"./tests/valid_configs/empty_config.toml\"])\n        .arg(\"-r\")\n        .arg(\"249\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"'--rate' must be greater\"));\n}\n\n#[test]\nfn test_large_default_time() {\n    no_cfg_btm_command()\n        .arg(\"-t\")\n        .arg(\"18446744073709551616\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\n            \"'--default_time_value' was set with an invalid value\",\n        ));\n}\n\n#[test]\nfn test_small_default_time() {\n    no_cfg_btm_command()\n        .arg(\"-t\")\n        .arg(\"900\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\n            \"'--default_time_value' must be greater\",\n        ));\n}\n\n#[test]\nfn test_large_delta_time() {\n    no_cfg_btm_command()\n        .arg(\"-d\")\n        .arg(\"18446744073709551616\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\n            \"'--time_delta' was set with an invalid value\",\n        ));\n}\n\n#[test]\nfn test_small_delta_time() {\n    no_cfg_btm_command()\n        .arg(\"-d\")\n        .arg(\"900\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"'--time_delta' must be greater\"));\n}\n\n#[test]\nfn test_large_rate() {\n    no_cfg_btm_command()\n        .arg(\"-r\")\n        .arg(\"18446744073709551616\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\n            \"'--rate' was set with an invalid value\",\n        ));\n}\n\n#[test]\nfn test_negative_rate() {\n    // This test should auto fail due to how clap works\n    no_cfg_btm_command()\n        .arg(\"-r\")\n        .arg(\"-1000\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"unexpected argument\"));\n}\n\n#[test]\nfn test_invalid_rate() {\n    no_cfg_btm_command()\n        .arg(\"-r\")\n        .arg(\"100-1000\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\n            \"'--rate' was set with an invalid value\",\n        ));\n}\n\n#[test]\nfn test_conflicting_temps() {\n    no_cfg_btm_command()\n        .arg(\"-c\")\n        .arg(\"-f\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"cannot be used with\"));\n}\n\n#[test]\nfn test_invalid_default_widget_1() {\n    no_cfg_btm_command()\n        .arg(\"--default_widget_type\")\n        .arg(\"fake_widget\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"invalid value\"));\n}\n\n#[test]\nfn test_invalid_default_widget_2() {\n    no_cfg_btm_command()\n        .arg(\"--default_widget_type\")\n        .arg(\"cpu\")\n        .arg(\"--default_widget_count\")\n        .arg(\"18446744073709551616\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"number too large\"));\n}\n\n#[test]\nfn test_missing_default_widget_type() {\n    no_cfg_btm_command()\n        .arg(\"--default_widget_count\")\n        .arg(\"3\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\n            \"the following required arguments were not provided\",\n        ));\n}\n\n#[test]\nfn test_invalid_default_cpu_entry() {\n    no_cfg_btm_command()\n        .arg(\"--default_cpu_entry\")\n        .arg(\"invalid\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"possible values\"));\n}\n\n#[test]\n#[cfg_attr(feature = \"battery\", ignore)]\nfn test_battery_flag() {\n    no_cfg_btm_command()\n        .arg(\"--battery\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\n            \"unexpected argument '--battery' found\",\n        ));\n}\n\n#[test]\n#[cfg_attr(feature = \"gpu\", ignore)]\nfn test_gpu_flag() {\n    no_cfg_btm_command()\n        .arg(\"--disable_gpu\")\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\n            \"unexpected argument '--disable_gpu' found\",\n        ));\n}\n\n/// Sanity test due to <https://github.com/ClementTsang/bottom/pull/1478>.\n#[test]\nfn test_version() {\n    btm_command(&[\"--version\"]).assert().success();\n    btm_command(&[\"-V\"]).assert().success();\n}\n\n/// Sanity test due to <https://github.com/ClementTsang/bottom/pull/1478>.\n#[test]\nfn test_help() {\n    btm_command(&[\"--help\"]).assert().success();\n    btm_command(&[\"-h\"]).assert().success();\n}\n"
  },
  {
    "path": "tests/integration/invalid_config_tests.rs",
    "content": "//! These tests are for testing some invalid config-file-specific options.\n\nuse assert_cmd::prelude::*;\nuse predicates::prelude::*;\n\nuse crate::util::btm_command;\n\n#[test]\nfn test_toml_mismatch_type() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/toml_mismatch_type.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"invalid type\"));\n}\n\n#[test]\nfn test_empty_layout() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/empty_layout.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"at least one widget\"));\n}\n\n#[test]\nfn test_invalid_layout_widget_type() {\n    btm_command(&[\n        \"-C\",\n        \"./tests/invalid_configs/invalid_layout_widget_type.toml\",\n    ])\n    .assert()\n    .failure()\n    .stderr(predicate::str::contains(\"invalid widget name\"));\n}\n\n/// This test isn't really needed as this is technically covered by TOML spec.\n/// However, I feel like it's worth checking anyways - not like it takes long.\n#[test]\nfn test_duplicate_temp_type() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/duplicate_temp_type.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"duplicate key\"));\n}\n\n/// Checks for if a hex is valid\n#[test]\nfn test_invalid_colour_hex() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/invalid_colour_hex.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"invalid hex color\"));\n}\n\n/// Checks for if a hex is too long\n#[test]\nfn test_invalid_colour_hex_2() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/invalid_colour_hex_2.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"invalid hex color\"));\n}\n\n/// Checks unicode hex because the way we originally did it could cause char\n/// boundary errors!\n#[test]\nfn test_invalid_colour_hex_3() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/invalid_colour_hex_3.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"invalid hex color\"));\n}\n\n#[test]\nfn test_invalid_colour_name() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/invalid_colour_name.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"invalid named color\"));\n}\n\n#[test]\nfn test_invalid_colour_rgb() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/invalid_colour_rgb.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"invalid RGB\"));\n}\n\n#[test]\nfn test_invalid_colour_rgb_2() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/invalid_colour_rgb_2.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"invalid RGB\"));\n}\n\n#[test]\nfn test_invalid_colour_string() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/invalid_colour_string.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"invalid named color\"));\n}\n\n#[test]\nfn test_lone_default_widget_count() {\n    btm_command(&[\n        \"-C\",\n        \"./tests/invalid_configs/lone_default_widget_count.toml\",\n    ])\n    .assert()\n    .failure()\n    .stderr(predicate::str::contains(\"it must be used with\"));\n}\n\n#[test]\nfn test_invalid_default_widget_count() {\n    btm_command(&[\n        \"-C\",\n        \"./tests/invalid_configs/invalid_default_widget_count.toml\",\n    ])\n    .assert()\n    .failure()\n    .stderr(predicate::str::contains(\"integer number overflowed\"));\n}\n\n#[test]\nfn test_invalid_process_column() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/invalid_process_column.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"doesn't match\"));\n}\n\n#[test]\nfn test_invalid_disk_column() {\n    btm_command(&[\"-C\", \"./tests/invalid_configs/invalid_disk_column.toml\"])\n        .assert()\n        .failure()\n        .stderr(predicate::str::contains(\"doesn't match\"));\n}\n"
  },
  {
    "path": "tests/integration/layout_movement_tests.rs",
    "content": "// TODO: Test basic mode\n// #[test]\n// fn test_basic_mode() {\n//     let ret_bottom_layout = BottomLayout::init_basic_default(false);\n// }\n\n// TODO: Test moving around with procs and their hidden children.\n\n// TODO: Test moving around with cpus if they get hidden.\n"
  },
  {
    "path": "tests/integration/main.rs",
    "content": "//! Integration tests for bottom.\n\n#![allow(clippy::unwrap_used)]\n#![allow(missing_docs)]\n\nmod util;\n\nmod arg_tests;\nmod invalid_config_tests;\nmod layout_movement_tests;\n\n#[cfg(all(target_arch = \"x86_64\", target_os = \"linux\"))]\nmod valid_config_tests;\n"
  },
  {
    "path": "tests/integration/util.rs",
    "content": "use std::{env, ffi::OsString, path::Path, process::Command};\n\n#[cfg(all(target_arch = \"x86_64\", target_os = \"linux\"))]\nuse portable_pty::{Child, CommandBuilder, MasterPty, PtySize, native_pty_system};\nuse rustc_hash::FxHashMap as HashMap;\n\npub fn abs_path(path: &str) -> OsString {\n    let path = Path::new(path);\n\n    if path.exists() {\n        path.canonicalize().unwrap().into_os_string()\n    } else {\n        // We are going to trust that the path given is valid...\n        path.to_owned().into_os_string()\n    }\n}\n\n/// Returns a QEMU runner target given an architecture.\nfn get_qemu_target(arch: &str) -> &str {\n    match arch {\n        \"armv7\" => \"arm\",\n        \"i686\" => \"i386\",\n        \"powerpc\" => \"ppc\",\n        \"powerpc64le\" => \"ppc64le\",\n        _ => arch,\n    }\n}\n\n/// This is required since running binary tests via cross can cause be tricky!\n/// We need to basically \"magically\" grab the correct runner in some cases,\n/// which can be done by inspecting env variables that should only show up while\n/// using cross.\n///\n/// Originally inspired by [ripgrep's test files](https://cs.github.com/BurntSushi/ripgrep/blob/9f0e88bcb14e02da1b88872435b17d74786640b5/tests/util.rs#L470),\n/// but adapted to work more generally with the architectures supported by\n/// bottom after looking through cross' [linux-runner](https://github.com/cross-rs/cross/blob/main/docker/linux-runner) file.\nfn cross_runner() -> Option<String> {\n    const TARGET_RUNNER: &str = \"CARGO_TARGET_RUNNER\";\n    const CROSS_RUNNER: &str = \"CROSS_RUNNER\";\n\n    let env_mapping = env::vars_os()\n        .filter_map(|(k, v)| {\n            let (k, v) = (k.to_string_lossy(), v.to_string_lossy());\n\n            if k.starts_with(\"CARGO_TARGET_\") && k.ends_with(\"_RUNNER\") && !v.is_empty() {\n                Some((TARGET_RUNNER.to_string(), v.to_string()))\n            } else if k == CROSS_RUNNER && !v.is_empty() {\n                Some((k.to_string(), v.to_string()))\n            } else {\n                None\n            }\n        })\n        .collect::<HashMap<_, _>>();\n\n    if let Some(cross_runner) = env_mapping.get(CROSS_RUNNER) {\n        if cross_runner == \"qemu-user\" {\n            env_mapping.get(TARGET_RUNNER).map(|target_runner| {\n                format!(\n                    \"qemu-{}\",\n                    get_qemu_target(target_runner.split_ascii_whitespace().last().unwrap())\n                )\n            })\n        } else {\n            None\n        }\n    } else {\n        env_mapping.get(TARGET_RUNNER).cloned()\n    }\n}\n\nconst BTM_EXE_PATH: &str = env!(\"CARGO_BIN_EXE_btm\");\nconst RUNNER_ENV_VARS: [(&str, &str); 1] = [(\"NO_COLOR\", \"1\")];\nconst DEFAULT_CFG: [&str; 2] = [\"-C\", \"./tests/valid_configs/empty_config.toml\"];\n\n/// Returns the [`Command`] of a binary invocation of bottom, alongside\n/// any required env variables.\npub fn btm_command(args: &[&str]) -> Command {\n    let mut cmd = match cross_runner() {\n        None => Command::new(BTM_EXE_PATH),\n        Some(runner) => {\n            let mut cmd = Command::new(runner);\n            cmd.envs(RUNNER_ENV_VARS);\n            cmd.arg(BTM_EXE_PATH);\n            cmd\n        }\n    };\n\n    let mut prev = \"\";\n    for arg in args.iter() {\n        if prev == \"-C\" {\n            // This is the config file; make sure we set it to absolute path!\n            cmd.arg(abs_path(arg));\n        } else {\n            cmd.arg(arg);\n        }\n\n        prev = arg;\n    }\n\n    cmd\n}\n\n/// Returns the [`Command`] of a binary invocation of bottom, alongside\n/// any required env variables, and with the default, empty config file.\npub fn no_cfg_btm_command() -> Command {\n    btm_command(&DEFAULT_CFG)\n}\n\n/// Spawns `btm` in a pty, returning the pair alongside a handle to the child.\n#[cfg(all(target_arch = \"x86_64\", target_os = \"linux\"))]\npub fn spawn_btm_in_pty(args: &[&str]) -> (Box<dyn MasterPty>, Box<dyn Child>) {\n    let native_pty = native_pty_system();\n\n    let pair = native_pty\n        .openpty(PtySize {\n            rows: 100,\n            cols: 100,\n            pixel_width: 1,\n            pixel_height: 1,\n        })\n        .unwrap();\n\n    let btm_exe = BTM_EXE_PATH;\n    let mut cmd = match cross_runner() {\n        None => CommandBuilder::new(btm_exe),\n        Some(runner) => {\n            let mut cmd = CommandBuilder::new(runner);\n            for (env, val) in RUNNER_ENV_VARS {\n                cmd.env(env, val);\n            }\n            cmd.arg(BTM_EXE_PATH);\n\n            cmd\n        }\n    };\n\n    let args = if args.is_empty() { &DEFAULT_CFG } else { args };\n    let mut prev = \"\";\n    for arg in args.iter() {\n        if prev == \"-C\" {\n            // This is the config file; make sure we set it to absolute path!\n            cmd.arg(abs_path(arg));\n        } else {\n            cmd.arg(arg);\n        }\n\n        prev = arg;\n    }\n\n    (pair.master, pair.slave.spawn_command(cmd).unwrap())\n}\n"
  },
  {
    "path": "tests/integration/valid_config_tests.rs",
    "content": "//! Tests config files that have sometimes caused issues despite being valid.\n\nuse std::{io::Read, thread, time::Duration};\n#[cfg(feature = \"default\")]\nuse std::{io::Write, path::Path};\n\nuse crate::util::spawn_btm_in_pty;\n\nfn reader_to_string(mut reader: Box<dyn Read>) -> String {\n    let mut buf = String::default();\n    reader.read_to_string(&mut buf).unwrap();\n\n    buf\n}\n\nfn run_and_kill(args: &[&str]) {\n    let (master, mut handle) = spawn_btm_in_pty(args);\n    let reader = master.try_clone_reader().unwrap();\n    let _ = master.take_writer().unwrap();\n\n    const TIMES_CHECKED: u64 = 6; // Check 6 times, once every 500ms, for 3 seconds total.\n\n    for _ in 0..TIMES_CHECKED {\n        thread::sleep(Duration::from_millis(500));\n        match handle.try_wait() {\n            Ok(Some(exit)) => {\n                println!(\"output: {}\", reader_to_string(reader));\n                panic!(\"program terminated unexpectedly (exit status: {exit:?})\");\n            }\n            Err(e) => {\n                println!(\"output: {}\", reader_to_string(reader));\n                panic!(\"error while trying to wait: {e}\")\n            }\n            _ => {}\n        }\n    }\n\n    handle.kill().unwrap();\n}\n\n#[test]\nfn test_basic() {\n    run_and_kill(&[]);\n}\n\n/// A test to ensure that a bad config will fail the `run_and_kill` function.\n#[test]\n#[should_panic]\nfn test_bad_basic() {\n    run_and_kill(&[\"--this_does_not_exist\"]);\n}\n\n#[test]\nfn test_empty() {\n    run_and_kill(&[\"-C\", \"./tests/valid_configs/empty_config.toml\"]);\n}\n\n#[cfg(feature = \"default\")]\nfn test_uncommented_default_config(original: &Path, test_name: &str) {\n    use regex::Regex;\n\n    // Take the default config file and uncomment everything.\n    let default_config = match std::fs::File::open(original) {\n        Ok(mut default_config_file) => {\n            let mut buf = String::new();\n            default_config_file\n                .read_to_string(&mut buf)\n                .expect(\"can read file\");\n\n            buf\n        }\n        Err(err) => {\n            println!(\"Could not open default config, skipping {test_name}. Error: {err:?}\");\n            return;\n        }\n    };\n\n    let default_config = Regex::new(r\"(?m)^#([a-zA-Z\\[])\")\n        .unwrap()\n        .replace_all(&default_config, \"$1\");\n\n    let default_config = Regex::new(r\"(?m)^#(\\s\\s+)([a-zA-Z\\[])\")\n        .unwrap()\n        .replace_all(&default_config, \"$2\");\n\n    let mut uncommented_config = match tempfile::NamedTempFile::new() {\n        Ok(tf) => tf,\n        Err(err) => {\n            println!(\"Could not create a temp file, skipping {test_name}. Error: {err:?}\");\n            return;\n        }\n    };\n\n    if let Err(err) = uncommented_config.write_all(default_config.as_bytes()) {\n        println!(\"Could not write to temp file, skipping {test_name}. Error: {err:?}\");\n        return;\n    }\n\n    run_and_kill(&[\"-C\", &uncommented_config.path().to_string_lossy()]);\n\n    uncommented_config.close().unwrap();\n}\n\n#[cfg(feature = \"default\")]\n#[test]\nfn test_default() {\n    test_uncommented_default_config(\n        Path::new(\"./sample_configs/default_config.toml\"),\n        \"test_default\",\n    );\n}\n\n#[cfg(feature = \"default\")]\n#[test]\nfn test_new_default() {\n    use tempfile::TempPath;\n\n    let new_temp_default_path = match tempfile::NamedTempFile::new() {\n        Ok(temp_file) => temp_file.into_temp_path(),\n        Err(err) => {\n            println!(\"Could not create a temp file, skipping test_new_default. Error: {err:?}\");\n            return;\n        }\n    };\n\n    // This is a hack because we need a temp file that doesn't exist.\n    let actual_temp_default_path = new_temp_default_path.to_path_buf();\n    new_temp_default_path.close().unwrap();\n\n    if !actual_temp_default_path.exists() {\n        run_and_kill(&[\"-C\", &(actual_temp_default_path.to_string_lossy())]);\n\n        // Re-take control over the temp path to ensure it gets deleted.\n        let actual_temp_default_path = TempPath::from_path(actual_temp_default_path);\n        test_uncommented_default_config(&actual_temp_default_path, \"test_new_default\");\n\n        actual_temp_default_path.close().unwrap();\n    } else {\n        println!(\"temp path we want to check exists, skip test_new_default test.\");\n    }\n}\n\n#[test]\nfn test_demo() {\n    let path: &str = \"./sample_configs/demo_config.toml\";\n    if std::path::Path::new(path).exists() {\n        run_and_kill(&[\"-C\", path]);\n    } else {\n        println!(\"Could not read demo config.\");\n    }\n}\n\n#[test]\nfn test_many_proc() {\n    run_and_kill(&[\"-C\", \"./tests/valid_configs/many_proc.toml\"]);\n}\n\n#[test]\nfn test_all_proc() {\n    run_and_kill(&[\"-C\", \"./tests/valid_configs/all_proc.toml\"]);\n}\n\n#[test]\nfn test_cpu_doughnut() {\n    run_and_kill(&[\"-C\", \"./tests/valid_configs/cpu_doughnut.toml\"]);\n}\n\n#[test]\nfn test_theme() {\n    run_and_kill(&[\"-C\", \"./tests/valid_configs/theme.toml\"]);\n}\n\n#[test]\nfn test_styling_sanity_check() {\n    run_and_kill(&[\"-C\", \"./tests/valid_configs/styling.toml\"]);\n}\n\n#[test]\nfn test_styling_sanity_check_2() {\n    run_and_kill(&[\"-C\", \"./tests/valid_configs/styling_2.toml\"]);\n}\n\n#[test]\nfn test_filtering() {\n    run_and_kill(&[\"-C\", \"./tests/valid_configs/filtering.toml\"]);\n}\n\n#[test]\nfn test_proc_columns() {\n    run_and_kill(&[\"-C\", \"./tests/valid_configs/proc_columns.toml\"]);\n}\n\n#[cfg(target_os = \"linux\")]\n#[test]\nfn test_linux_only() {\n    run_and_kill(&[\"-C\", \"./tests/valid_configs/os_specific/linux.toml\"]);\n}\n"
  },
  {
    "path": "tests/invalid_configs/duplicate_temp_type.toml",
    "content": "[flags]\ntemperature_type = \"k\"\ntemperature_type = \"f\"\ntemperature_type = \"c\""
  },
  {
    "path": "tests/invalid_configs/empty_layout.toml",
    "content": "[[row]]"
  },
  {
    "path": "tests/invalid_configs/invalid_colour_hex.toml",
    "content": "[styles.tables.headers]\ncolor=\"#zzzzzz\""
  },
  {
    "path": "tests/invalid_configs/invalid_colour_hex_2.toml",
    "content": "[styles.tables.headers]\ncolor=\"#1111111\""
  },
  {
    "path": "tests/invalid_configs/invalid_colour_hex_3.toml",
    "content": "[styles.tables.headers]\ncolor = \"#加拿大\"\n"
  },
  {
    "path": "tests/invalid_configs/invalid_colour_name.toml",
    "content": "[styles.tables.headers]\ncolor = \"LightB lue\"\n"
  },
  {
    "path": "tests/invalid_configs/invalid_colour_rgb.toml",
    "content": "[styles.tables.headers]\ncolor=\"257, 50, 50\""
  },
  {
    "path": "tests/invalid_configs/invalid_colour_rgb_2.toml",
    "content": "[styles.tables.headers]\ncolor=\"50, 50, 50, 50\""
  },
  {
    "path": "tests/invalid_configs/invalid_colour_string.toml",
    "content": "[styles.tables.headers]\ncolor=\"this is not a colour\""
  },
  {
    "path": "tests/invalid_configs/invalid_default_widget_count.toml",
    "content": "[flags]\ndefault_widget_type=\"CPU\"\ndefault_widget_count=18446744073709551616\n"
  },
  {
    "path": "tests/invalid_configs/invalid_disk_column.toml",
    "content": "[disk]\ncolumns = [\"disk\", \"fake\"]\n"
  },
  {
    "path": "tests/invalid_configs/invalid_layout_widget_type.toml",
    "content": "[[row]]\n    [[row.child]]\n        type=\"cpu\"\n    [[row.child]]\n        type=\"not_real\"\n        "
  },
  {
    "path": "tests/invalid_configs/invalid_process_column.toml",
    "content": "[processes]\ncolumns = [\"cpu\", \"fake\"]\n"
  },
  {
    "path": "tests/invalid_configs/lone_default_widget_count.toml",
    "content": "[flags]\ndefault_widget_count = 3\n"
  },
  {
    "path": "tests/invalid_configs/toml_mismatch_type.toml",
    "content": "[flags]\nbasic = \"test\"\n"
  },
  {
    "path": "tests/valid_configs/all_proc.toml",
    "content": "[[row]]\nratio = 30\n[[row.child]]\ntype = \"proc\"\n[[row]]\nratio = 40\n[[row.child]]\nratio = 4\ntype = \"proc\"\n[[row.child]]\nratio = 3\n[[row.child.child]]\ntype = \"proc\"\n[[row.child.child]]\ntype = \"proc\"\n[[row]]\nratio = 30\n[[row.child]]\ntype = \"proc\"\n[[row.child]]\ntype = \"proc\"\ndefault = true\n"
  },
  {
    "path": "tests/valid_configs/cpu_doughnut.toml",
    "content": "[[row]]\n[[row.child]]\ntype = \"cpu\"\n[[row.child]]\ntype = \"cpu\"\n[[row.child]]\ntype = \"cpu\"\n\n[[row]]\n[[row.child]]\n[[row.child.child]]\ntype = \"cpu\"\n[[row.child.child]]\ntype = \"cpu\"\n[[row.child]]\ntype = \"empty\"\n[[row.child]]\n[[row.child.child]]\ntype = \"cpu\"\n[[row.child.child]]\ntype = \"cpu\"\n\n[[row]]\n[[row.child]]\ntype = \"cpu\"\n[[row.child]]\ntype = \"cpu\"\n[[row.child]]\ntype = \"cpu\"\n"
  },
  {
    "path": "tests/valid_configs/empty_config.toml",
    "content": "# This config file is \"empty\" so running tests does not modify a user's home directory.\n"
  },
  {
    "path": "tests/valid_configs/filtering.toml",
    "content": "[disk]\n[disk.name_filter]\nis_list_ignored = true\nlist = [\"/dev/sda\\\\d+\", \"/dev/nvme0n1p2\"]\nregex = true\ncase_sensitive = false\nwhole_word = false\n\n[disk.mount_filter]\nis_list_ignored = true\nlist = [\"/mnt/.*\", \"/boot\"]\nregex = true\ncase_sensitive = false\nwhole_word = false\n\n[temperature]\n[temperature.sensor_filter]\nis_list_ignored = true\nlist = [\"cpu\", \"wifi\"]\nregex = false\ncase_sensitive = false\nwhole_word = false\n\n[network]\n[network.interface_filter]\nis_list_ignored = true\nlist = [\"virbr0.*\"]\nregex = true\ncase_sensitive = false\nwhole_word = false\n"
  },
  {
    "path": "tests/valid_configs/many_proc.toml",
    "content": "[[row]]\nratio = 30\n[[row.child]]\ntype = \"cpu\"\n[[row]]\nratio = 40\n[[row.child]]\nratio = 4\ntype = \"mem\"\n[[row.child]]\nratio = 3\n[[row.child.child]]\ntype = \"proc\"\n[[row.child.child]]\ntype = \"proc\"\n[[row]]\nratio = 30\n[[row.child]]\ntype = \"net\"\n[[row.child]]\ntype = \"proc\"\ndefault = true\n"
  },
  {
    "path": "tests/valid_configs/os_specific/linux.toml",
    "content": "# Remove things from this if they are no longer OS-specific.\n\n[styles.widgets]\nthread_text = { color = \"green\" }\n"
  },
  {
    "path": "tests/valid_configs/proc_columns.toml",
    "content": "[processes]\ncolumns = [\"PID\", \"Name\", \"CPU%\", \"Mem%\", \"Virt\", \"Rps\", \"Wps\", \"TRead\", \"Twrite\", \"User\", \"State\", \"Time\"]\n"
  },
  {
    "path": "tests/valid_configs/styling.toml",
    "content": "# Test basic colours\n[styles.cpu]\nall_entry_color = \"255, 50, 50\"\n\n# Test tables\n[styles.graphs.legend_text]\ncolor = \"#fff\"\nbg_color = \"#000\"\nbold = false\n\n# Test inline tables\n[styles.tables]\nheaders = { color = \"red\", bg_color = \"black\", bold = true }\n\n# Test italics\n[styles.widgets.widget_title]\ncolor = \"#0f0f0f\"\nbg_color = \"#f0f0f0\"\nbold = true\nitalics = true\n\n# Test using normal colour where inline table can also work\n[styles.widgets]\nselected_text = \"#fff\"\ndisabled_text = \"blue\"\ntext = \"255, 0, 255\"\n"
  },
  {
    "path": "tests/valid_configs/styling_2.toml",
    "content": "# These are all the components that support custom theming.  Note that colour support\n# will depend on terminal support.\n[styles]\n\n# Built-in themes. Valid values are:\n# - \"default\"\n# - \"default-light\"\n# - \"gruvbox\"\n# - \"gruvbox-light\"\n# - \"nord\"\n# - \"nord-light\".\n\n# This will have the lowest precedence if a custom colour palette is set,\n# or overridden if the command-line flag for a built-in theme is set.\ntheme = \"default\"\n\n[styles.cpu]\nall_entry_color = \"green\"\navg_entry_color = \"red\"\ncpu_core_colors = [\"light magenta\", \"light yellow\", \"light cyan\", \"light green\", \"light blue\", \"cyan\", \"green\", \"blue\"]\n\n[styles.memory]\nram_color = \"light magenta\"\ncache_color = \"light red\"\nswap_color = \"light yellow\"\narc_color = \"light cyan\"\ngpu_colors = [\"light blue\", \"light red\", \"cyan\", \"green\", \"blue\", \"red\"]\n\n[styles.network]\nrx_color = \"light magenta\"\ntx_color = \"light yellow\"\nrx_total_color = \"light cyan\"\ntx_total_color = \"light green\"\n\n[styles.battery]\nhigh_battery_color = \"green\"\nmedium_battery_color = \"yellow\"\nlow_battery_color = \"red\"\n\n[styles.tables]\nheaders = { color = \"light blue\" }\n\n[styles.graphs]\ngraph_color = \"gray\"\nlegend_text = { color = \"gray\" }\n\n[styles.widgets]\nborder_color = \"gray\"\nselected_border_color = \"light blue\"\nwidget_title = { color = \"gray\" }\ntext = { color = \"gray\" }\nselected_text = { color = \"black\", bg_color = \"light blue\" }\ndisabled_text = { color = \"dark gray\" }\n"
  },
  {
    "path": "tests/valid_configs/theme.toml",
    "content": "#:schema none\n# Adding this to avoid a warning from some schema linters\n\n[styles]\ntheme = \"gruvbox\"\n"
  },
  {
    "path": "wix/main.wxs",
    "content": "<?xml version='1.0' encoding='windows-1252'?>\n<!--\n  Copyright (C) 2017 Christopher R. Field.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n  http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n-->\n\n<!--\n  The \"cargo wix\" subcommand provides a variety of predefined variables available\n  for customization of this template. The values for each variable are set at\n  installer creation time. The following variables are available:\n\n  TargetTriple      = The rustc target triple name.\n  TargetEnv         = The rustc target environment. This is typically either\n                      \"msvc\" or \"gnu\" depending on the toolchain downloaded and\n                      installed.\n  TargetVendor      = The rustc target vendor. This is typically \"pc\", but Rust\n                      does support other vendors, like \"uwp\".\n  CargoTargetBinDir = The complete path to the directory containing the\n                      binaries (exes) to include. The default would be\n                      \"target\\release\\\". If an explicit rustc target triple is\n                      used, i.e. cross-compiling, then the default path would\n                      be \"target\\<CARGO_TARGET>\\<CARGO_PROFILE>\",\n                      where \"<CARGO_TARGET>\" is replaced with the \"CargoTarget\"\n                      variable value and \"<CARGO_PROFILE>\" is replaced with the\n                      value from the \"CargoProfile\" variable. This can also\n                      be overridden manually with the \"target-bin-dir\" flag.\n  CargoTargetDir    = The path to the directory for the build artifacts, i.e.\n                      \"target\".\n  CargoProfile      = The cargo profile used to build the binaries\n                      (usually \"debug\" or \"release\").\n  Version           = The version for the installer. The default is the\n                      \"Major.Minor.Fix\" semantic versioning number of the Rust\n                      package.\n-->\n\n<!--\n  Please do not remove these pre-processor If-Else blocks. These are used with\n  the `cargo wix` subcommand to automatically determine the installation\n  destination for 32-bit versus 64-bit installers. Removal of these lines will\n  cause installation errors.\n-->\n<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?>\n    <?define PlatformProgramFilesFolder = \"ProgramFiles64Folder\" ?>\n<?else ?>\n    <?define PlatformProgramFilesFolder = \"ProgramFilesFolder\" ?>\n<?endif ?>\n\n<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>\n\n    <Product\n        Id='*'\n        Name='bottom'\n        UpgradeCode='3C90C27D-8372-4C82-B03C-020393CB983D'\n        Manufacturer='Clement Tsang'\n        Language='1033'\n        Codepage='1252'\n        Version='$(var.Version)'>\n\n        <Package Id='*'\n            Keywords='Installer'\n            Description='A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows.'\n            Manufacturer='Clement Tsang'\n            InstallerVersion='450'\n            Languages='1033'\n            Compressed='yes'\n            InstallScope='perMachine'\n            SummaryCodepage='1252'\n            />\n\n        <MajorUpgrade\n            Schedule='afterInstallInitialize'\n            DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>\n\n        <Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>\n        <Property Id='DiskPrompt' Value='bottom Installation'/>\n\n        <Directory Id='TARGETDIR' Name='SourceDir'>\n            <Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>\n                <Directory Id='APPLICATIONFOLDER' Name='bottom'>\n                    <!--\n                      Disabling the license sidecar file in the installer is a two step process:\n\n                      1. Comment out or remove the `Component` tag along with its contents.\n                      2. Comment out or remove the `ComponentRef` tag with the \"License\" Id\n                         attribute value further down in this file.\n                    -->\n                    <Component Id='License' Guid='*'>\n                        <File Id='LicenseFile'\n                            DiskId='1'\n                            Source='wix\\License.rtf'\n                            KeyPath='yes'/>\n                    </Component>\n\n                    <Directory Id='Completions' Name='completions'>\n                      <Component Id='btmPowershell' Guid='*'>\n                        <File\n                            Id='btmPs1'\n                            Name='_btm.ps1'\n                            DiskId='1'\n                            Source='$(var.CargoTargetDir)\\tmp\\bottom\\completion\\_btm.ps1'\n                            KeyPath='yes'/>\n                      </Component>\n\n                      <Component Id='btmNu' Guid='*'>\n                        <File\n                            Id='btmNu'\n                            Name='btm.nu'\n                            DiskId='1'\n                            Source='$(var.CargoTargetDir)\\tmp\\bottom\\completion\\btm.nu'\n                            KeyPath='yes'/>\n                      </Component>\n\n                      <Component Id='btmFish' Guid='*'>\n                        <File\n                            Id='btmFish'\n                            Name='btm.fish'\n                            DiskId='1'\n                            Source='$(var.CargoTargetDir)\\tmp\\bottom\\completion\\btm.fish'\n                            KeyPath='yes'/>\n                      </Component>\n\n                      <Component Id='btmBash' Guid='*'>\n                        <File\n                            Id='btmBash'\n                            Name='btm.bash'\n                            DiskId='1'\n                            Source='$(var.CargoTargetDir)\\tmp\\bottom\\completion\\btm.bash'\n                            KeyPath='yes'/>\n                      </Component>\n\n                      <Component Id='btmZsh' Guid='*'>\n                        <File\n                            Id='btmZsh'\n                            Name='_btm'\n                            DiskId='1'\n                            Source='$(var.CargoTargetDir)\\tmp\\bottom\\completion\\_btm'\n                            KeyPath='yes'/>\n                      </Component>\n                    </Directory>\n                    \n                    <Directory Id='Bin' Name='bin'>\n                        <Component Id='Path' Guid='15D841CF-0363-4DBB-BF55-ECCB43B9EB03' KeyPath='yes'>\n                            <Environment\n                                Id='PATH'\n                                Name='PATH'\n                                Value='[Bin]'\n                                Permanent='no'\n                                Part='last'\n                                Action='set'\n                                System='yes'/>\n                        </Component>\n                        <Component Id='binary0' Guid='*'>\n                            <File\n                                Id='exe0'\n                                Name='btm.exe'\n                                DiskId='1'\n                                Source='$(var.CargoTargetBinDir)\\btm.exe'\n                                KeyPath='yes'/>\n                        </Component>\n                    </Directory>\n                </Directory>\n            </Directory>\n        </Directory>\n\n        <Feature\n            Id='Binaries'\n            Title='Application'\n            Description='Installs all binaries and the license.'\n            Level='1'\n            ConfigurableDirectory='APPLICATIONFOLDER'\n            AllowAdvertise='no'\n            Display='expand'\n            Absent='disallow'>\n            <!--\n              Comment out or remove the following `ComponentRef` tag to remove\n              the license sidecar file from the installer.\n            -->\n            <ComponentRef Id='License'/>\n            \n            <ComponentRef Id='btmPowershell'/>\n            <ComponentRef Id='btmNu'/>\n            <ComponentRef Id='btmFish'/>\n            <ComponentRef Id='btmBash'/>\n            <ComponentRef Id='btmZsh'/>\n\n            <ComponentRef Id='binary0'/>\n\n            <Feature\n                Id='Environment'\n                Title='PATH Environment Variable'\n                Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.'\n                Level='1'\n                Absent='allow'>\n                <ComponentRef Id='Path'/>\n            </Feature>\n        </Feature>\n\n        <SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>\n\n        \n        <!--\n          Uncomment the following `Icon` and `Property` tags to change the product icon.\n\n          The product icon is the graphic that appears in the Add/Remove\n          Programs control panel for the application.\n        -->\n        <!--<Icon Id='ProductICO' SourceFile='wix\\Product.ico'/>-->\n        <!--<Property Id='ARPPRODUCTICON' Value='ProductICO' />-->\n\n        <Property Id='ARPHELPLINK' Value='https://bottom.pages.dev/stable'/>\n        \n        <UI>\n            <UIRef Id='WixUI_FeatureTree'/>\n            <!--\n              Disabling the EULA dialog in the installer is a two step process:\n\n                 1. Uncomment the following two `Publish` tags\n                 2. Comment out or remove the `<WiXVariable Id='WixUILicenseRtf'...` tag further down\n\n            -->\n            <!--<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>-->\n            <!--<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>-->\n            \n        </UI>\n\n        <!--\n          Disabling the EULA dialog in the installer requires commenting out\n          or removing the following `WixVariable` tag\n        -->\n        <WixVariable Id='WixUILicenseRtf' Value='wix\\License.rtf'/>\n        \n        \n        <!--\n          Uncomment the next `WixVariable` tag to customize the installer's\n          Graphical User Interface (GUI) and add a custom banner image across\n          the top of each screen. See the WiX Toolset documentation for details\n          about customization.\n\n          The banner BMP dimensions are 493 x 58 pixels.\n        -->\n        <!--<WixVariable Id='WixUIBannerBmp' Value='wix\\Banner.bmp'/>-->\n\n        \n        <!--\n          Uncomment the next `WixVariable` tag to customize the installer's\n          Graphical User Interface (GUI) and add a custom image to the first\n          dialog, or screen. See the WiX Toolset documentation for details about\n          customization.\n\n          The dialog BMP dimensions are 493 x 312 pixels.\n        -->\n        <!--<WixVariable Id='WixUIDialogBmp' Value='wix\\Dialog.bmp'/>-->\n\n    </Product>\n\n</Wix>\n"
  }
]