From f541445e91292b3001243e30e3047e2f78055d7d Mon Sep 17 00:00:00 2001 From: Ayush Dumasia Date: Sat, 3 May 2025 18:36:46 +0530 Subject: [PATCH] =?UTF-8?q?feat=20=E2=9C=A8:=20Yazi=20plugins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- git/config | 3 +- waybar/scripts/volume-bar.sh | 22 +++ yazi/init.lua | 32 ++++ yazi/keymap.toml | 4 + yazi/package.toml | 12 ++ yazi/plugins/git.yazi/LICENSE | 21 +++ yazi/plugins/git.yazi/README.md | 78 +++++++++ yazi/plugins/git.yazi/main.lua | 228 ++++++++++++++++++++++++++ yazi/plugins/vcs-files.yazi/LICENSE | 21 +++ yazi/plugins/vcs-files.yazi/README.md | 29 ++++ yazi/plugins/vcs-files.yazi/main.lua | 33 ++++ yazi/yazi.toml | 10 ++ 12 files changed, 492 insertions(+), 1 deletion(-) create mode 100755 waybar/scripts/volume-bar.sh create mode 100644 yazi/init.lua create mode 100644 yazi/keymap.toml create mode 100644 yazi/package.toml create mode 100644 yazi/plugins/git.yazi/LICENSE create mode 100644 yazi/plugins/git.yazi/README.md create mode 100644 yazi/plugins/git.yazi/main.lua create mode 100644 yazi/plugins/vcs-files.yazi/LICENSE create mode 100644 yazi/plugins/vcs-files.yazi/README.md create mode 100644 yazi/plugins/vcs-files.yazi/main.lua diff --git a/git/config b/git/config index 09b6ea3..fa4d165 100644 --- a/git/config +++ b/git/config @@ -26,6 +26,7 @@ context = 3 renames = copies interHunkContext = 10 + [pager] diff = diff-so-fancy | less --tabs=4 -RFX @@ -40,7 +41,7 @@ old = red [interactive] - diffFilter = diff-so-fancy --patch + # diffFilter = diff-so-fancy --patch singleKey = true [commit] diff --git a/waybar/scripts/volume-bar.sh b/waybar/scripts/volume-bar.sh new file mode 100755 index 0000000..5ef09e3 --- /dev/null +++ b/waybar/scripts/volume-bar.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +volume=$(pamixer --get-volume) +muted=$(pamixer --get-mute) + +if [ "$muted" = "true" ]; then + echo '{"text": "[Muted]"}' + exit +fi + +bar="" +full_blocks=$((volume / 10)) +empty_blocks=$((10 - full_blocks)) + +for ((i=0; i [!NOTE] +> Yazi v25.2.26 or later is required for this plugin to work. + +Show the status of Git file changes as linemode in the file list. + +https://github.com/user-attachments/assets/34976be9-a871-4ffe-9d5a-c4cdd0bf4576 + +## Installation + +```sh +ya pack -a yazi-rs/plugins:git +``` + +## Setup + +Add the following to your `~/.config/yazi/init.lua`: + +```lua +require("git"):setup() +``` + +And register it as fetchers in your `~/.config/yazi/yazi.toml`: + +```toml +[[plugin.prepend_fetchers]] +id = "git" +name = "*" +run = "git" + +[[plugin.prepend_fetchers]] +id = "git" +name = "*/" +run = "git" +``` + +## Advanced + +You can customize the [Style](https://yazi-rs.github.io/docs/plugins/layout#style) of the status sign with: + +- `th.git.modified` +- `th.git.added` +- `th.git.untracked` +- `th.git.ignored` +- `th.git.deleted` +- `th.git.updated` + +For example: + +```lua +-- ~/.config/yazi/init.lua +th.git = th.git or {} +th.git.modified = ui.Style():fg("blue") +th.git.deleted = ui.Style():fg("red"):bold() +``` + +You can also customize the text of the status sign with: + +- `th.git.modified_sign` +- `th.git.added_sign` +- `th.git.untracked_sign` +- `th.git.ignored_sign` +- `th.git.deleted_sign` +- `th.git.updated_sign` + +For example: + +```lua +-- ~/.config/yazi/init.lua +th.git = th.git or {} +th.git.modified_sign = "M" +th.git.deleted_sign = "D" +``` + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/yazi/plugins/git.yazi/main.lua b/yazi/plugins/git.yazi/main.lua new file mode 100644 index 0000000..d8f365a --- /dev/null +++ b/yazi/plugins/git.yazi/main.lua @@ -0,0 +1,228 @@ +--- @since 25.4.4 + +local WINDOWS = ya.target_family() == "windows" + +-- The code of supported git status, +-- also used to determine which status to show for directories when they contain different statuses +-- see `bubble_up` +local CODES = { + excluded = 100, -- ignored directory + ignored = 6, -- ignored file + untracked = 5, + modified = 4, + added = 3, + deleted = 2, + updated = 1, + unknown = 0, +} + +local PATTERNS = { + { "!$", CODES.ignored }, + { "?$", CODES.untracked }, + { "[MT]", CODES.modified }, + { "[AC]", CODES.added }, + { "D", CODES.deleted }, + { "U", CODES.updated }, + { "[AD][AD]", CODES.updated }, +} + +local function match(line) + local signs = line:sub(1, 2) + for _, p in ipairs(PATTERNS) do + local path, pattern, code = nil, p[1], p[2] + if signs:find(pattern) then + path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4) + path = WINDOWS and path:gsub("/", "\\") or path + end + if not path then + elseif path:find("[/\\]$") then + -- Mark the ignored directory as `excluded`, so we can process it further within `propagate_down` + return code == CODES.ignored and CODES.excluded or code, path:sub(1, -2) + else + return code, path + end + end +end + +local function root(cwd) + local is_worktree = function(url) + local file, head = io.open(tostring(url)), nil + if file then + head = file:read(8) + file:close() + end + return head == "gitdir: " + end + + repeat + local next = cwd:join(".git") + local cha = fs.cha(next) + if cha and (cha.is_dir or is_worktree(next)) then + return tostring(cwd) + end + cwd = cwd.parent + until not cwd +end + +local function bubble_up(changed) + local new, empty = {}, Url("") + for path, code in pairs(changed) do + if code ~= CODES.ignored then + local url = Url(path).parent + while url and url ~= empty do + local s = tostring(url) + new[s] = (new[s] or CODES.unknown) > code and new[s] or code + url = url.parent + end + end + end + return new +end + +local function propagate_down(excluded, cwd, repo) + local new, rel = {}, cwd:strip_prefix(repo) + for _, path in ipairs(excluded) do + if rel:starts_with(path) then + -- If `cwd` is a subdirectory of an excluded directory, also mark it as `excluded` + new[tostring(cwd)] = CODES.excluded + elseif cwd == repo:join(path).parent then + -- If `path` is a direct subdirectory of `cwd`, mark it as `ignored` + new[path] = CODES.ignored + else + -- Skipping, we only care about `cwd` itself and its direct subdirectories for maximum performance + end + end + return new +end + +local add = ya.sync(function(st, cwd, repo, changed) + st.dirs[cwd] = repo + st.repos[repo] = st.repos[repo] or {} + for path, code in pairs(changed) do + if code == CODES.unknown then + st.repos[repo][path] = nil + elseif code == CODES.excluded then + -- Mark the directory with a special value `excluded` so that it can be distinguished during UI rendering + st.dirs[path] = CODES.excluded + else + st.repos[repo][path] = code + end + end + ya.render() +end) + +local remove = ya.sync(function(st, cwd) + local repo = st.dirs[cwd] + if not repo then + return + end + + ya.render() + st.dirs[cwd] = nil + if not st.repos[repo] then + return + end + + for _, r in pairs(st.dirs) do + if r == repo then + return + end + end + st.repos[repo] = nil +end) + +local function setup(st, opts) + st.dirs = {} -- Mapping between a directory and its corresponding repository + st.repos = {} -- Mapping between a repository and the status of each of its files + + opts = opts or {} + opts.order = opts.order or 1500 + + local t = th.git or {} + local styles = { + [CODES.ignored] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("darkgray"), + [CODES.untracked] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("magenta"), + [CODES.modified] = t.modified and ui.Style(t.modified) or ui.Style():fg("yellow"), + [CODES.added] = t.added and ui.Style(t.added) or ui.Style():fg("green"), + [CODES.deleted] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("red"), + [CODES.updated] = t.updated and ui.Style(t.updated) or ui.Style():fg("yellow"), + } + local signs = { + [CODES.ignored] = t.ignored_sign or "", + [CODES.untracked] = t.untracked_sign or "?", + [CODES.modified] = t.modified_sign or "", + [CODES.added] = t.added_sign or "", + [CODES.deleted] = t.deleted_sign or "", + [CODES.updated] = t.updated_sign or "", + } + + Linemode:children_add(function(self) + local url = self._file.url + local repo = st.dirs[tostring(url.base)] + local code + if repo then + code = repo == CODES.excluded and CODES.ignored or st.repos[repo][tostring(url):sub(#repo + 2)] + end + + if not code or signs[code] == "" then + return "" + elseif self._file.is_hovered then + return ui.Line { " ", signs[code] } + else + return ui.Line { " ", ui.Span(signs[code]):style(styles[code]) } + end + end, opts.order) +end + +local function fetch(_, job) + local cwd = job.files[1].url.base + local repo = root(cwd) + if not repo then + remove(tostring(cwd)) + return true + end + + local paths = {} + for _, file in ipairs(job.files) do + paths[#paths + 1] = tostring(file.url) + end + + -- stylua: ignore + local output, err = Command("git") + :cwd(tostring(cwd)) + :args({ "--no-optional-locks", "-c", "core.quotePath=", "status", "--porcelain", "-unormal", "--no-renames", "--ignored=matching" }) + :args(paths) + :stdout(Command.PIPED) + :output() + if not output then + return true, Err("Cannot spawn `git` command, error: %s", err) + end + + local changed, excluded = {}, {} + for line in output.stdout:gmatch("[^\r\n]+") do + local code, path = match(line) + if code == CODES.excluded then + excluded[#excluded + 1] = path + else + changed[path] = code + end + end + + if job.files[1].cha.is_dir then + ya.dict_merge(changed, bubble_up(changed)) + end + ya.dict_merge(changed, propagate_down(excluded, cwd, Url(repo))) + + -- Reset the status of any files that don't appear in the output of `git status` to `unknown`, + -- so that cleaning up outdated statuses from `st.repos` + for _, path in ipairs(paths) do + local s = path:sub(#repo + 2) + changed[s] = changed[s] or CODES.unknown + end + + add(tostring(cwd), repo, changed) + + return false +end + +return { setup = setup, fetch = fetch } diff --git a/yazi/plugins/vcs-files.yazi/LICENSE b/yazi/plugins/vcs-files.yazi/LICENSE new file mode 100644 index 0000000..fb5b1d6 --- /dev/null +++ b/yazi/plugins/vcs-files.yazi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 yazi-rs + +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/vcs-files.yazi/README.md b/yazi/plugins/vcs-files.yazi/README.md new file mode 100644 index 0000000..a86dccf --- /dev/null +++ b/yazi/plugins/vcs-files.yazi/README.md @@ -0,0 +1,29 @@ +# vcs-files.yazi + +Show Git file changes in Yazi. + +https://github.com/user-attachments/assets/465b801b-3516-4f57-be09-8405da21e34d + +## Installation + +```sh +ya pack -a yazi-rs/plugins:vcs-files +``` + +## Usage + +```toml +# keymap.toml +[[manager.prepend_keymap]] +on = [ "g", "c" ] +run = "plugin vcs-files" +desc = "Show Git file changes" +``` + +## TODO + +- [ ] Add support for other VCS (e.g. Mercurial, Subversion) + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/yazi/plugins/vcs-files.yazi/main.lua b/yazi/plugins/vcs-files.yazi/main.lua new file mode 100644 index 0000000..516a037 --- /dev/null +++ b/yazi/plugins/vcs-files.yazi/main.lua @@ -0,0 +1,33 @@ +--- @since 25.4.8 + +local root = ya.sync(function() return cx.active.current.cwd end) + +local function fail(content) return ya.notify { title = "VCS Files", content = content, timeout = 5, level = "error" } end + +local function entry() + local root = root() + local output, err = Command("git"):cwd(tostring(root)):args({ "diff", "--name-only", "HEAD" }):output() + if err then + return fail("Failed to run `git diff`, error: " .. err) + elseif not output.status.success then + return fail("Failed to run `git diff`, stderr: " .. output.stderr) + end + + local id = ya.id("ft") + local cwd = root:into_search("Git changes") + ya.mgr_emit("cd", { Url(cwd) }) + ya.mgr_emit("update_files", { op = fs.op("part", { id = id, url = Url(cwd), files = {} }) }) + + local files = {} + for line in output.stdout:gmatch("[^\r\n]+") do + local url = cwd:join(line) + local cha = fs.cha(url, true) + if cha then + files[#files + 1] = File { url = url, cha = cha } + end + end + ya.mgr_emit("update_files", { op = fs.op("part", { id = id, url = Url(cwd), files = files }) }) + ya.mgr_emit("update_files", { op = fs.op("done", { id = id, url = cwd, cha = Cha { kind = 16 } }) }) +end + +return { entry = entry } diff --git a/yazi/yazi.toml b/yazi/yazi.toml index afb3fba..07f9ac4 100644 --- a/yazi/yazi.toml +++ b/yazi/yazi.toml @@ -218,3 +218,13 @@ sort_by = "none" sort_sensitive = false sort_reverse = false sort_translit = false + +[[plugin.prepend_fetchers]] +id = "git" +name = "*" +run = "git" + +[[plugin.prepend_fetchers]] +id = "git" +name = "*/" +run = "git"