From 9a0df75a91e13c2a2f1f091f73852df5ced65f90 Mon Sep 17 00:00:00 2001 From: Michael Carlberg Date: Sun, 13 Nov 2016 06:09:51 +0100 Subject: [PATCH] feat(fs): New filesystem module Module that displays details about mounted filesystems, #84 Closes #153 --- examples/CMakeLists.txt | 2 +- examples/config | 20 ++++- examples/config.cmake | 18 ++++ include/modules/fs.hpp | 77 +++++++++++++++++ include/utils/mtab.hpp | 36 ++++++++ include/utils/string.hpp | 2 + src/components/controller.cpp | 3 + src/modules/fs.cpp | 150 ++++++++++++++++++++++++++++++++++ src/utils/string.cpp | 38 ++++++++- 9 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 include/modules/fs.hpp create mode 100644 include/utils/mtab.hpp create mode 100644 src/modules/fs.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c5df1e67..97caf746 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,7 +3,7 @@ # set(MODULES_LEFT "bspwm i3 mpd") -set(MODULES_CENTER "") +set(MODULES_CENTER "filesystem") set(MODULES_RIGHT "backlight volume memory cpu wlan eth battery temperature date powermenu") # Strip disabled modules {{{ diff --git a/examples/config b/examples/config index 5ec3104f..5928e374 100644 --- a/examples/config +++ b/examples/config @@ -41,7 +41,7 @@ font-1 = unifont:size=6;-2 font-2 = siji:pixelsize=10;0 modules-left = bspwm i3 mpd -modules-center = +modules-center = filesystem modules-right = backlight volume memory cpu wlan eth battery temperature date powermenu tray-position = right @@ -52,6 +52,24 @@ tray-padding = 4 ;wm-restack = bspwm +[module/filesystem] +type = internal/fs +interval = 25 + +disk-0 = / +disk-1 = /home +disk-2 = /invalid/mountpoint + +;fixed-values = true +;spacing = 4 + +label-mounted = %mountpoint%: %percentage_free% + +label-unmounted = %mountpoint%: not mounted +label-unmounted-foreground = #55 + + + [module/bspwm] type = internal/bspwm ws-icon-default = x diff --git a/examples/config.cmake b/examples/config.cmake index 413deee6..87dfa434 100644 --- a/examples/config.cmake +++ b/examples/config.cmake @@ -52,6 +52,24 @@ tray-padding = 4 ;wm-restack = bspwm +[module/filesystem] +type = internal/fs +interval = 25 + +disk-0 = / +disk-1 = /home +disk-2 = /invalid/mountpoint + +;fixed-values = true +;spacing = 4 + +label-mounted = %mountpoint%: %percentage_free% + +label-unmounted = %mountpoint%: not mounted +label-unmounted-foreground = #55 + + + [module/bspwm] type = internal/bspwm ws-icon-default = x diff --git a/include/modules/fs.hpp b/include/modules/fs.hpp new file mode 100644 index 00000000..fc0105ea --- /dev/null +++ b/include/modules/fs.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include "components/config.hpp" +#include "config.hpp" +#include "drawtypes/label.hpp" +#include "drawtypes/progressbar.hpp" +#include "drawtypes/ramp.hpp" +#include "modules/meta.hpp" + +LEMONBUDDY_NS + +namespace modules { + /** + * Filesystem structure + */ + struct fs_disk { + string mountpoint; + bool mounted = false; + + string type; + string fsname; + + unsigned long long bytes_free = 0; + unsigned long long bytes_used = 0; + unsigned long long bytes_total = 0; + + float percentage_free = 0; + float percentage_used = 0; + + string percentage_free_s; + string percentage_used_s; + + explicit fs_disk(const string& mountpoint, bool mounted = false) + : mountpoint(mountpoint), mounted(mounted) {} + }; + + using fs_disk_t = unique_ptr; + + /** + * Module used to display filesystem stats. + */ + class fs_module : public timer_module { + public: + using timer_module::timer_module; + + void setup(); + bool update(); + string get_format() const; + string get_output(); + bool build(builder* builder, string tag) const; + + private: + static constexpr auto FORMAT_MOUNTED = "format-mounted"; + static constexpr auto FORMAT_UNMOUNTED = "format-unmounted"; + static constexpr auto TAG_LABEL_MOUNTED = ""; + static constexpr auto TAG_LABEL_UNMOUNTED = ""; + static constexpr auto TAG_BAR_USED = ""; + static constexpr auto TAG_BAR_FREE = ""; + static constexpr auto TAG_RAMP_CAPACITY = ""; + + label_t m_labelmounted; + label_t m_labelunmounted; + progressbar_t m_barused; + progressbar_t m_barfree; + ramp_t m_rampcapacity; + + vector m_mounts; + vector m_disks; + bool m_fixed = false; + int m_spacing = 2; + + // used while formatting output + size_t m_index = 0; + }; +} + +LEMONBUDDY_NS_END diff --git a/include/utils/mtab.hpp b/include/utils/mtab.hpp new file mode 100644 index 00000000..e0982d9f --- /dev/null +++ b/include/utils/mtab.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "common.hpp" + +LEMONBUDDY_NS + +namespace mtab_util { + /** + * Wrapper for reading mtab entries + */ + class reader { + public: + explicit reader() { + if ((m_ptr = setmntent("/etc/mtab", "r")) == nullptr) { + throw system_error("Failed to read mtab"); + } + } + + ~reader() { + if (m_ptr != nullptr) { + endmntent(m_ptr); + } + } + + bool next(mntent** dst) { + return (*dst = getmntent(m_ptr)) != nullptr; + } + + protected: + FILE* m_ptr = nullptr; + }; +} + +LEMONBUDDY_NS_END diff --git a/include/utils/string.hpp b/include/utils/string.hpp index 70014a05..56d65550 100644 --- a/include/utils/string.hpp +++ b/include/utils/string.hpp @@ -28,6 +28,8 @@ namespace string_util { vector& split_into(string s, char delim, vector& container); vector split(const string& s, char delim); size_t find_nth(string haystack, size_t pos, string needle, size_t nth); + string floatval(float value, int decimals = 2, bool fixed = false, string locale = ""); + string filesize(unsigned long long bytes, int decimals = 2, bool fixed = false, string locale = ""); string from_stream(const std::basic_ostream& os); hash_type hash(string src); } diff --git a/src/components/controller.cpp b/src/components/controller.cpp index cd9dd96c..20cd1f3a 100644 --- a/src/components/controller.cpp +++ b/src/components/controller.cpp @@ -9,6 +9,7 @@ #include "modules/counter.hpp" #include "modules/cpu.hpp" #include "modules/date.hpp" +#include "modules/fs.hpp" #include "modules/memory.hpp" #include "modules/menu.hpp" #include "modules/script.hpp" @@ -343,6 +344,8 @@ void controller::bootstrap_modules() { module.reset(new cpu_module(bar, m_log, m_conf, module_name)); else if (type == "internal/date") module.reset(new date_module(bar, m_log, m_conf, module_name)); + else if (type == "internal/fs") + module.reset(new fs_module(bar, m_log, m_conf, module_name)); else if (type == "internal/memory") module.reset(new memory_module(bar, m_log, m_conf, module_name)); else if (type == "internal/i3") diff --git a/src/modules/fs.cpp b/src/modules/fs.cpp new file mode 100644 index 00000000..cdfd9811 --- /dev/null +++ b/src/modules/fs.cpp @@ -0,0 +1,150 @@ +#include + +#include "modules/fs.hpp" +#include "utils/math.hpp" +#include "utils/mtab.hpp" +#include "utils/string.hpp" + +LEMONBUDDY_NS + +namespace modules { + /** + * Bootstrap the module by reading config values and + * setting up required components + */ + void fs_module::setup() { + m_mounts = m_conf.get_list(name(), "disk"); + m_fixed = m_conf.get(name(), "fixed-values", m_fixed); + m_spacing = m_conf.get(name(), "spacing", m_spacing); + m_interval = chrono::duration(m_conf.get(name(), "interval", 30)); + + // Add formats and elements + m_formatter->add( + FORMAT_MOUNTED, TAG_LABEL_MOUNTED, {TAG_LABEL_MOUNTED, TAG_BAR_FREE, TAG_BAR_USED, TAG_RAMP_CAPACITY}); + m_formatter->add( + FORMAT_UNMOUNTED, TAG_LABEL_UNMOUNTED, {TAG_LABEL_UNMOUNTED, TAG_BAR_FREE, TAG_BAR_USED, TAG_RAMP_CAPACITY}); + + if (m_formatter->has(TAG_LABEL_MOUNTED)) + m_labelmounted = load_optional_label(m_conf, name(), TAG_LABEL_MOUNTED, "%mountpoint% %percentage_free%"); + if (m_formatter->has(TAG_LABEL_UNMOUNTED)) + m_labelunmounted = load_optional_label(m_conf, name(), TAG_LABEL_UNMOUNTED, "%mountpoint% is not mounted"); + if (m_formatter->has(TAG_BAR_FREE)) + m_barfree = load_progressbar(m_bar, m_conf, name(), TAG_BAR_FREE); + if (m_formatter->has(TAG_BAR_USED)) + m_barused = load_progressbar(m_bar, m_conf, name(), TAG_BAR_USED); + if (m_formatter->has(TAG_RAMP_CAPACITY)) + m_rampcapacity = load_ramp(m_conf, name(), TAG_RAMP_CAPACITY); + } + + /** + * Update disk values by reading mtab entries + */ + bool fs_module::update() { + m_disks.clear(); + + struct statvfs buffer; + struct mntent* mount = nullptr; + + for (auto&& mountpoint : m_mounts) { + m_disks.emplace_back(new fs_disk{mountpoint, false}); + + if (statvfs(mountpoint.c_str(), &buffer) == -1) { + continue; + } + + auto mtab = make_unique(); + auto& disk = m_disks.back(); + + while (mtab->next(&mount)) { + if (strncmp(mount->mnt_dir, mountpoint.c_str(), strlen(mount->mnt_dir)) != 0) { + continue; + } + + disk->mounted = true; + disk->mountpoint = mount->mnt_dir; + disk->type = mount->mnt_type; + disk->fsname = mount->mnt_fsname; + + auto b_total = buffer.f_bsize * buffer.f_blocks; + auto b_free = buffer.f_bsize * buffer.f_bfree; + auto b_used = b_total - b_free; + + disk->bytes_total = b_total; + disk->bytes_free = b_free; + disk->bytes_used = b_used; + + disk->percentage_free = math_util::percentage(b_free, 0, b_total); + disk->percentage_used = math_util::percentage(b_used, 0, b_total); + + disk->percentage_free_s = string_util::floatval(disk->percentage_free, 2, m_fixed, m_bar.locale); + disk->percentage_used_s = string_util::floatval(disk->percentage_used, 2, m_fixed, m_bar.locale); + } + } + + return true; + } + + /** + * Generate the module output + */ + string fs_module::get_output() { + string output; + + for (m_index = 0; m_index < m_disks.size(); ++m_index) { + if (!output.empty()) + m_builder->space(m_spacing); + output += timer_module::get_output(); + } + + return output; + } + + /** + * Select format based on fs state + */ + string fs_module::get_format() const { + return m_disks[m_index]->mounted ? FORMAT_MOUNTED : FORMAT_UNMOUNTED; + } + + /** + * Output content using configured format tags + */ + bool fs_module::build(builder* builder, string tag) const { + auto& disk = m_disks[m_index]; + + if (tag == TAG_BAR_FREE) { + builder->node(m_barfree->output(disk->percentage_free)); + } else if (tag == TAG_BAR_USED) { + builder->node(m_barused->output(disk->percentage_used)); + } else if (tag == TAG_RAMP_CAPACITY) { + builder->node(m_rampcapacity->get_by_percentage(disk->percentage_free)); + } else if (tag == TAG_LABEL_MOUNTED || tag == TAG_LABEL_UNMOUNTED) { + label_t label; + + if (tag == TAG_LABEL_MOUNTED) + label = m_labelmounted->clone(); + else + label = m_labelunmounted->clone(); + + label->reset_tokens(); + label->replace_token("%mountpoint%", disk->mountpoint); + label->replace_token("%type%", disk->type); + label->replace_token("%fsname%", disk->fsname); + + label->replace_token("%percentage_free%", disk->percentage_free_s + "%"); + label->replace_token("%percentage_used%", disk->percentage_used_s + "%"); + + label->replace_token("%total%", string_util::filesize(disk->bytes_total, 1, m_fixed, m_bar.locale)); + label->replace_token("%free%", string_util::filesize(disk->bytes_free, 2, m_fixed, m_bar.locale)); + label->replace_token("%used%", string_util::filesize(disk->bytes_used, 2, m_fixed, m_bar.locale)); + + builder->node(label); + } else { + return false; + } + + return true; + } +} + +LEMONBUDDY_NS_END diff --git a/src/utils/string.cpp b/src/utils/string.cpp index 077801d1..f3bd4e3f 100644 --- a/src/utils/string.cpp +++ b/src/utils/string.cpp @@ -157,12 +157,48 @@ namespace string_util { return find_nth(haystack, found_pos + 1, needle, nth - 1); } + /** + * Create a float value string + */ + string floatval(float value, int decimals, bool fixed, string locale) { + stringstream ss; + ss.precision(decimals); + if (!locale.empty()) + ss.imbue(std::locale(locale.c_str())); + if (fixed) + ss << std::fixed; + ss << value; + return ss.str(); + } + + /** + * Format a filesize string + */ + string filesize(unsigned long long bytes, int decimals, bool fixed, string locale) { + vector suffixes{"TB", "GB", "MB"}; + string suffix{"KB"}; + + while ((bytes /= 1000) > 999) { + suffix = suffixes.back(); + suffixes.pop_back(); + } + + stringstream ss; + ss.precision(decimals); + if (!locale.empty()) + ss.imbue(std::locale(locale.c_str())); + if (fixed) + ss << std::fixed; + ss << bytes << " " << suffix; + return ss.str(); + } + /** * Get the resulting string from a ostream/ * * Example usage: * @code cpp - * string_util::from_stream(std::stringstream() << ...); + * string_util::from_stream(stringstream() << ...); * @endcode */ string from_stream(const std::basic_ostream& os) {