summaryrefslogtreecommitdiff
path: root/yazi/plugins/fs-usage.yazi
diff options
context:
space:
mode:
Diffstat (limited to 'yazi/plugins/fs-usage.yazi')
-rw-r--r--yazi/plugins/fs-usage.yazi/LICENSE21
-rw-r--r--yazi/plugins/fs-usage.yazi/README.md93
-rw-r--r--yazi/plugins/fs-usage.yazi/main.lua292
3 files changed, 406 insertions, 0 deletions
diff --git a/yazi/plugins/fs-usage.yazi/LICENSE b/yazi/plugins/fs-usage.yazi/LICENSE
new file mode 100644
index 0000000..18e9226
--- /dev/null
+++ b/yazi/plugins/fs-usage.yazi/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 walldmtd
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/yazi/plugins/fs-usage.yazi/README.md b/yazi/plugins/fs-usage.yazi/README.md
new file mode 100644
index 0000000..1544634
--- /dev/null
+++ b/yazi/plugins/fs-usage.yazi/README.md
@@ -0,0 +1,93 @@
+# fs-usage.yazi
+
+A [Yazi](https://github.com/sxyazi/yazi) plugin to show the current partition's used space in the header or status.
+
+![preview_normal.png](previews/preview_normal.png)
+
+![preview_warning.png](previews/preview_warning.png)
+
+## Installation
+
+> [!IMPORTANT]
+>
+> - This plugin is only supported on Linux (uses `df`)
+> - It *might* work with WSL, but that is untested
+> - Requires Yazi v25.5.31 or later
+
+Install with `ya`:
+
+```sh
+ya pkg add walldmtd/fs-usage
+```
+
+## Usage
+
+To use the default setup, add this somewhere in `~/.config/yazi/init.lua`:
+
+```lua
+require("fs-usage"):setup()
+```
+
+To customize it, add this instead and adjust/remove the options as needed:
+
+```lua
+require("fs-usage"):setup({
+ -- All values are optional
+
+ -- Position of the component
+ -- parent: Parent component ("Header" or "Status")
+ -- align: Anchor point within parent object ("LEFT" or "RIGHT")
+ -- order: Order relative to others in the same parent
+ -- Default: { parent = "Header", align = "RIGHT", order = 2000 }
+ position = { parent = "Header", align = "RIGHT", order = 2000 },
+
+ -- Text format
+ -- One of:
+ -- "both": Partition name and percent used
+ -- "name": Only partition name
+ -- "usage": Only percent used
+ -- Default: "both"
+ format = "both",
+
+ -- Option to enable or disable the usage bar
+ -- Default: true
+ bar = true,
+
+ -- Percent usage to use the warning style (-1 to disable)
+ -- Default: 90
+ warning_threshold = 90,
+
+ -- For style options, any unset options use the progress bar style
+ -- from the Yazi flavor if available, otherwise it falls back to
+ -- the default Yazi style.
+
+ -- Label text style
+ -- fg: Text colour (String like "white", or hex like "#ffffff")
+ -- (Can also be "" to use the reverse of the bar colour)
+ -- bold: Make the label bold (bool)
+ -- italic: Make the label italic (bool)
+ -- Example: style_label = { fg = "", bold = true, italic = false },
+ -- Default: {}
+ style_label = {},
+
+ -- Usage bar style
+ -- fg: Bar colour (String like "blue", or hex like "#0000ff")
+ -- bg: Bar background colour (Same format as fg)
+ -- Example: style_normal = { fg = "blue", bg = "black" },
+ -- Default: {}
+ style_normal = {},
+
+ -- Usage bar style when the used space is above the warning threshold
+ -- Options are the same as style_normal
+ -- Default: {}
+ style_warning = {},
+
+ -- Bar padding
+ -- open: Character on the left (string)
+ -- close: Character on the right (string)
+ -- Example: { open = "█", close = "█" } for square corners,
+ -- or { open = "", close = "" } for no padding
+ -- Default: { open = "", close = "" }
+ padding = { open = "", close = "" }
+})
+```
diff --git a/yazi/plugins/fs-usage.yazi/main.lua b/yazi/plugins/fs-usage.yazi/main.lua
new file mode 100644
index 0000000..dbcdef2
--- /dev/null
+++ b/yazi/plugins/fs-usage.yazi/main.lua
@@ -0,0 +1,292 @@
+--- @since 25.5.31
+
+local DEFAULT_OPTIONS = {
+ -- Can't reference Header.RIGHT etc. here (it hangs) so parent and align are strings
+ -- 2000 puts it to the right of the indicator, and leaves some room between
+ position = { parent = "Header", align = "RIGHT", order = 2000 },
+ format = "both",
+ bar = true,
+ warning_threshold = 90,
+ style_label = {
+ fg = th.status.progress_label:fg(),
+ },
+ style_normal = {
+ fg = th.status.progress_normal:fg(),
+ bg = th.status.progress_normal:bg(),
+ },
+ style_warning = {
+ fg = th.status.progress_error:fg(),
+ bg = th.status.progress_error:bg(),
+ },
+ padding = { open = "", close = "" },
+}
+
+---Deep copy and merge two tables, overwriting values from one table into another
+---@param from Table to take values from
+---@param to Table to merge into
+local function merge(into, from)
+ -- Handle nil inputs
+ into = into or {}
+ from = from or {}
+
+ local result = {}
+
+ -- Deep copy 'into' first
+ for k, v in pairs(into) do
+ if type(v) == "table" then
+ result[k] = merge({}, v)
+ else
+ result[k] = v
+ end
+ end
+
+ -- Merge
+ for k, v in pairs(from) do
+ if type(v) == "table" then
+ result[k] = merge(result[k], v)
+ else
+ result[k] = v
+ end
+ end
+
+ return result
+end
+
+---Merge label and bar styles into left/right styles for the bar
+---@param style_label Label style
+---@param style_bar Usage bar style
+local function build_styles(style_label, style_bar)
+ local style_right = ui.Style():fg(style_label.fg or style_bar.fg):bg(style_bar.bg) -- Label bg is ignored
+ if style_label.bold then
+ style_right = style_right:bold()
+ end
+ if style_label.italic then
+ style_right = style_right:italic()
+ end
+
+ -- Left style is the same as right, but with fg/bg reversed
+ -- (this is overridden by the label colour if set)
+ local style_left = ui.Style():patch(style_right):fg(style_label.fg or style_bar.bg):bg(style_bar.fg) -- Label bg is ignored
+
+ return style_left, style_right
+end
+
+---Parse source and usage from df output
+---@param stdout string df output
+local function process_df_output(stdout)
+ local source, usage = stdout:match(".*%s(%S+)%s+(%S+)")
+
+ -- Follow symlinks in source
+ if string.sub(source, 1, 1) == "/" then
+ source = Command("readlink"):arg({ "--silent", "--canonicalize", "--no-newline", source }):output().stdout
+ end
+
+ -- Assume usage is a valid percentage
+ -- Remove the percent and parse
+ usage = tonumber(string.sub(usage, 1, #usage - 1))
+
+ return source, usage
+end
+
+---Format text based on options
+---@param source Source
+---@param usage Usage
+---@param format Format
+local function format_text(source, usage, format)
+ local text = ""
+ if format == "both" then
+ text = string.format("%s: %d%%", source, usage)
+ elseif format == "name" then
+ text = string.format("%s", source)
+ elseif format == "usage" then
+ text = string.format("%d%%", usage)
+ end
+ return text
+end
+
+---Set new plugin state and redraw
+local set_state = ya.sync(function(st, source, usage, text, bar_len)
+ st.source = source
+ st.usage = usage
+ st.text = text
+ st.bar_len = bar_len
+
+ local render = ui.render or ya.render
+ render()
+end)
+
+---Get plugin state needed by entry
+local get_state = ya.sync(function(st)
+ return {
+ -- Persistent options
+ format = st.format,
+ bar = st.bar,
+ padding = st.padding,
+
+ -- Variables
+ source = st.source,
+ usage = st.usage,
+ }
+end)
+
+-- Called from init.lua
+---@param st State
+---@param opts Options
+local function setup(st, opts)
+ opts = merge(DEFAULT_OPTIONS, opts)
+
+ -- Allow unsetting some options
+ if opts.style_label.fg == "" then
+ opts.style_label.fg = nil
+ end
+ if opts.warning_threshold < 0 then
+ opts.warning_threshold = nil
+ end
+
+ -- Translate opts.position.parent option into a component reference
+ if opts.position.parent == "Header" then
+ opts.position.parent = Header
+ elseif opts.position.parent == "Status" then
+ opts.position.parent = Status
+ else
+ -- Just set it to nil, it's gonna cause errors anyway
+ opts.position.parent = nil
+ end
+
+ -- Set persistent options
+ local style_normal_left, style_normal_right = build_styles(opts.style_label, opts.style_normal)
+ local style_warning_left, style_warning_right = build_styles(opts.style_label, opts.style_warning)
+ st.style = {
+ normal = {
+ left = style_normal_left,
+ right = style_normal_right,
+ padding = {
+ left = ui.Style():fg(style_normal_left:bg()),
+ right = ui.Style():fg(style_normal_right:bg()),
+ },
+ },
+ warning = {
+ left = style_warning_left,
+ right = style_warning_right,
+ padding = {
+ left = ui.Style():fg(style_warning_left:bg()),
+ right = ui.Style():fg(style_warning_right:bg()),
+ },
+ },
+ }
+ st.format = opts.format
+ st.bar = opts.bar
+ st.warning_threshold = opts.warning_threshold
+ st.padding = opts.padding
+
+ -- Add the component to the parent
+ opts.position.parent:children_add(function(self)
+ -- No point showing anything if usage is nil
+ if not st.usage then
+ return
+ end
+
+ local style = (st.warning_threshold and st.usage >= st.warning_threshold) and st.style.warning
+ or st.style.normal
+
+ -- Apply styles to components based on the bar length, and ad them to the bar
+ local output = {}
+ local bar_len = st.bar_len
+ local components = {
+ { text = st.padding.open, style = style.padding },
+ { text = st.text, style = style },
+ { text = st.padding.close, style = style.padding },
+ }
+ for _, component in ipairs(components) do
+ -- bar_len_bytes should point to the last byte that should be coloured by the usage bar
+ local bar_len_bytes
+ if bar_len <= 0 then
+ -- 1-indexed, so effectively no bar showing
+ bar_len_bytes = 0
+ elseif bar_len >= utf8.len(component.text) then
+ bar_len_bytes = #component.text
+ else
+ bar_len_bytes = utf8.offset(component.text, bar_len + 1) - 1
+ end
+
+ if bar_len_bytes > 0 then
+ table.insert(output, ui.Span(string.sub(component.text, 1, bar_len_bytes)):style(component.style.left))
+ end
+ if bar_len_bytes < #component.text then
+ table.insert(
+ output,
+ ui.Span(string.sub(component.text, bar_len_bytes + 1)):style(component.style.right)
+ )
+ end
+
+ bar_len = bar_len - utf8.len(component.text)
+ end
+
+ return ui.Line(output)
+ end, opts.position.order, opts.position.parent[opts.position.align])
+
+ ---Pass cwd to the plugin for df
+ local function callback()
+ ya.emit("plugin", {
+ st._id,
+ ya.quote(tostring(cx.active.current.cwd), true),
+ })
+ end
+
+ -- Subscribe to events
+ ps.sub("cd", callback)
+ ps.sub("tab", callback)
+ ps.sub("delete", callback)
+ -- These are the only relevant events that actually work
+ -- Note: df might not immediately reflect usage changes
+ -- when deleting files
+end
+
+-- Called from ya.emit in the callback
+---@param job Job
+local function entry(_, job)
+ local cwd = job.args[1]
+
+ -- Don't set cwd directly for Command() here, it hangs for dirs without read perms
+ -- cwd is fine as an argument to df though
+ local output = Command("df"):arg({ "--output=source,pcent", tostring(cwd) }):output()
+
+ -- If df fails, hide the module
+ if not output.status.success then
+ set_state(nil, nil, nil, nil)
+ return
+ end
+
+ local source, usage = process_df_output(output.stdout)
+
+ -- If df read the filesystem but couldn't get a percentage, hide the module
+ -- if usage == nil then
+ if type(usage) ~= "number" then
+ set_state(nil, nil, nil, nil)
+ return
+ end
+
+ -- Get the plugin state (async) early since we know it will be used
+ local st = get_state()
+
+ -- If nothing has changed, don't bother updating
+ if source == st.source and usage == st.usage then
+ return
+ end
+
+ local text = format_text(source, usage, st.format)
+ local bar_len = 0 -- Start with no bar by default
+
+ -- Only calculate bar length if the bar will be shown
+ if st.bar then
+ local total_len = utf8.len(st.padding.open .. text .. st.padding.close)
+
+ -- Using ceil so the bar is only empty at 0%
+ -- Using len - 1 so the bar isn't full until 100%
+ bar_len = usage < 100 and math.ceil((total_len - 1) / 100 * usage) or total_len
+ end
+
+ set_state(source, usage, text, bar_len)
+end
+
+return { setup = setup, entry = entry }