diff --git a/yazi/keymap.toml b/yazi/keymap.toml index 63faf9f..8d598d2 100644 --- a/yazi/keymap.toml +++ b/yazi/keymap.toml @@ -4,8 +4,8 @@ [manager] prepend_keymap = [ - { on = "i", run = 'plugin --sync smart-enter' }, - { on = "", run = 'plugin --sync smart-enter' }, + { on = "i", run = 'plugin smart-enter' }, + { on = "", run = 'plugin smart-enter' }, { on = "S", run = 'shell "$SHELL" --block --confirm' }, { on = [ "'", @@ -60,11 +60,11 @@ keymap = [ { on = "I", run = "forward", desc = "Go forward to the next directory" }, # Selection - { on = "", run = ["select --state=none", "arrow 1"], desc = "Toggle the current selection state" }, - { on = "v", run = "visual_mode", desc = "Enter visual mode (selection mode)" }, - { on = "V", run = "visual_mode --unset", desc = "Enter visual mode (unset mode)" }, - { on = "", run = "select_all --state=true", desc = "Select all files" }, - { on = "", run = "select_all --state=none", desc = "Inverse selection of all files" }, + { on = "", run = ["toggle", "arrow 1"], desc = "Toggle the current selection state" }, + { on = "v", run = "toggle_all", desc = "Enter visual mode (selection mode)" }, + # { on = "V", run = "visual_mode --unset", desc = "Enter visual mode (unset mode)" }, + # { on = "", run = "select_all --state=true", desc = "Select all files" }, + # { on = "", run = "select_all --state=none", desc = "Inverse selection of all files" }, # Find { on = "", run = "plugin fzf", desc = "Jump to a directory or reveal a file using fzf" }, @@ -121,18 +121,18 @@ keymap = [ # Linemode { on = ["m", "s"], run = "linemode size", desc = "Set linemode to size" }, - { on = ["m", "p"], run = "linemode permissions", desc = "Set linemode to permissions" }, - { on = ["m", "c"], run = "linemode ctime", desc = "Set linemode to ctime" }, + { on = ["m", "p"], run = "linemode perm", desc = "Set linemode to permissions" }, + { on = ["m", "c"], run = "linemode btime", desc = "Set linemode to btime" }, { on = ["m", "m"], run = "linemode mtime", desc = "Set linemode to mtime" }, { on = ["m", "o"], run = "linemode owner", desc = "Set linemode to owner" }, { on = ["m", "n"], run = "linemode none", desc = "Set linemode to none" }, # Sorting - { on = ["o", "M"], run = ["sort modified --reverse=no", "linemode mtime"], desc = "Sort by modified time" }, - { on = ["o", "m"], run = ["sort modified --reverse", "linemode mtime"], desc = "Sort by modified time (reverse)" }, - { on = ["o", "C"], run = ["sort created --reverse=no", "linemode ctime"], desc = "Sort by created time" }, - { on = ["o", "c"], run = ["sort created --reverse", "linemode ctime"], desc = "Sort by created time (reverse)" }, + { on = ["o", "M"], run = ["sort mtime --reverse=no", "linemode mtime"], desc = "Sort by modified time" }, + { on = ["o", "m"], run = ["sort mtime --reverse", "linemode mtime"], desc = "Sort by modified time (reverse)" }, + { on = ["o", "C"], run = ["sort btime --reverse=no", "linemode btime"], desc = "Sort by created time" }, + { on = ["o", "c"], run = ["sort btime --reverse", "linemode btime"], desc = "Sort by created time (reverse)" }, { on = ["o", "E"], run = "sort extension --reverse=no", desc = "Sort by extension" }, { on = ["o", "e"], run = "sort extension --reverse", desc = "Sort by extension (reverse)" }, { on = ["o", "a"], run = "sort alphabetical --reverse=no", desc = "Sort alphabetically" }, diff --git a/yazi/package.toml b/yazi/package.toml index b43cea4..03e96e2 100644 --- a/yazi/package.toml +++ b/yazi/package.toml @@ -1,5 +1,5 @@ [plugin] -deps = [{ use = "llanosrocas/yaziline", rev = "5886330" }, { use = "Rolv-Apneseth/starship", rev = "20d5a4d" }] +deps = [{ use = "llanosrocas/yaziline", rev = "5886330" }, { use = "Rolv-Apneseth/starship", rev = "247f49d" }, { use = "yazi-rs/plugins:git", rev = "ec97f88" }] [flavor] deps = [] diff --git a/yazi/plugins/git.yazi/DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY b/yazi/plugins/git.yazi/DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY new file mode 100644 index 0000000..e69de29 diff --git a/yazi/plugins/git.yazi/LICENSE b/yazi/plugins/git.yazi/LICENSE index 85c5dfd..fb5b1d6 100644 --- a/yazi/plugins/git.yazi/LICENSE +++ b/yazi/plugins/git.yazi/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 DreamMaoMao +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 diff --git a/yazi/plugins/git.yazi/README.md b/yazi/plugins/git.yazi/README.md index e999026..fad9ce4 100644 --- a/yazi/plugins/git.yazi/README.md +++ b/yazi/plugins/git.yazi/README.md @@ -1,43 +1,81 @@ # git.yazi -git message prompt plugin for Yazi, -Asynchronous task loading without blocking the rendering of other components +> [!NOTE] +> Yazi v0.3.3 or later is required for this plugin to work. -![image](https://gitee.com/DreamMaoMao/git.yazi/assets/30348075/3a95e25a-cf0e-4f03-8d92-e7c9cc0767bb) +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 - -https://gitee.com/DreamMaoMao/git.yazi/assets/30348075/f827dd33-8e51-4f8a-9069-0affc2f7aab8 - - - -# Install - -### Linux - -```bash -git clone https://gitee.com/DreamMaoMao/git.yazi.git ~/.config/yazi/plugins/git.yazi +```sh +ya pack -a yazi-rs/plugins:git ``` -# Dependcy -- git +## Setup -# Usage +Add the following to your `~/.config/yazi/init.lua`: -Add this to ~/.config/yazi/init.lua +```lua +require("git"):setup() +``` -``` -require("git"):setup{ -} -``` -if you want listen for file changes to automatically update the status. -Add this to ~/.config/yazi/yazi.toml, `below the exists [plugin] modules`, like this -``` -[plugin] +And register it as fetchers in your `~/.config/yazi/yazi.toml`: -fetchers = [ - { id = "git", name = "*", run = "git", prio = "normal" }, - { id = "git", name = "*/", run = "git", prio = "normal" }, -] +```toml +[[plugin.prepend_fetchers]] +id = "git" +name = "*" +run = "git" + +[[plugin.prepend_fetchers]] +id = "git" +name = "*/" +run = "git" ``` + +## Advanced + +> [!NOTE] +> This section currently requires Yazi nightly that includes https://github.com/sxyazi/yazi/pull/1637 + +You can customize the [Style](https://yazi-rs.github.io/docs/plugins/layout#style) of the status sign with: + +- `THEME.git.modified` +- `THEME.git.added` +- `THEME.git.untracked` +- `THEME.git.ignored` +- `THEME.git.deleted` +- `THEME.git.updated` + +For example: + +```lua +-- ~/.config/yazi/init.lua +THEME.git = THEME.git or {} +THEME.git.modified = ui.Style():fg("blue") +THEME.git.deleted = ui.Style():fg("red"):bold() +``` + +You can also customize the text of the status sign with: + +- `THEME.git.modified_sign` +- `THEME.git.added_sign` +- `THEME.git.untracked_sign` +- `THEME.git.ignored_sign` +- `THEME.git.deleted_sign` +- `THEME.git.updated_sign` + +For example: + +```lua +-- ~/.config/yazi/init.lua +THEME.git = THEME.git or {} +THEME.git.modified_sign = "M" +THEME.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/init.lua b/yazi/plugins/git.yazi/init.lua index 15b58cb..a64b749 100644 --- a/yazi/plugins/git.yazi/init.lua +++ b/yazi/plugins/git.yazi/init.lua @@ -1,214 +1,211 @@ -local function string_split(input,delimiter) +local WIN = ya.target_family() == "windows" +local PATS = { + { "[MT]", 6 }, -- Modified + { "[AC]", 5 }, -- Added + { "?$", 4 }, -- Untracked + { "!$", 3 }, -- Ignored + { "D", 2 }, -- Deleted + { "U", 1 }, -- Updated + { "[AD][AD]", 1 }, -- Updated +} - local result = {} - - for match in (input..delimiter):gmatch("(.-)"..delimiter) do - table.insert(result, match) - end - return result -end - -local function set_status_color(status) - if status == nil then - return "#6cc749" - elseif status == "M" then - return "#ec613f" - elseif status == "A" then - return "#ec613f" - elseif status == "." then - return "#ae96ee" - elseif status == "?" then - return "#D4BB91" - elseif status == "R" then - return "#ec613f" - else - return "#ec613f" - end - -end - -local function fix_str_ch(str) - local chinese_chars, num_replacements = str:gsub("\\(%d%d%d)", function (s) - return string.char(tonumber(s, 8)) - end) - return num_replacements > 0 and chinese_chars:sub(2,-2) or chinese_chars -end - -local function make_git_table(git_status_str) - local file_table = {} - local git_status - local is_dirty = false - local filename - local multi_path - local is_ignore_dir = false - local is_untracked_dir = false - local convert_name - local split_table = string_split(git_status_str:sub(1,-2),"\n") - for _, value in ipairs(split_table) do - split_value = string_split(value," ") - if split_value[#split_value - 1] == "" then - split_value = string_split(value," ") +local function match(line) + local signs = line:sub(1, 2) + for _, p in ipairs(PATS) do + local path + if signs:find(p[1]) then + path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4) + path = WIN and path:gsub("/", "\\") or path end - - if split_value[#split_value - 1] == "??" then - git_status = "?" - is_dirty = true - elseif split_value[#split_value - 1] == "!!" then - git_status = "." - elseif split_value[#split_value - 1] == "->" then - git_status = "R" - is_dirty = true + if not path then + elseif path:find("[/\\]$") then + return p[2] == 3 and 30 or p[2], path:sub(1, -2) else - git_status = split_value[#split_value - 1] - is_dirty = true + return p[2], path end - if split_value[#split_value]:sub(-2,-1) == "./" and git_status == "." then - is_ignore_dir = true - return file_table,is_dirty,is_ignore_dir,is_untracked_dir - end - - if split_value[#split_value]:sub(-2,-1) == "./" and git_status == "?" then - is_untracked_dir = true - return file_table,is_dirty,is_ignore_dir,is_untracked_dir - end - - multi_path = string_split(split_value[#split_value],"/") - if (multi_path[#multi_path] == "" and #multi_path == 2) or git_status ~= "." then - filename = multi_path[1] - else - filename = split_value[#split_value] - end - - convert_name = fix_str_ch(filename) - file_table[convert_name] = git_status end - - return file_table,is_dirty,is_ignore_dir,is_untracked_dir end -local save = ya.sync(function(st, cwd, git_branch,git_file_status,git_is_dirty,git_status_str,is_ignore_dir,is_untracked_dir) - if cx.active.current.cwd == Url(cwd) then - st.git_branch = git_branch - st.git_file_status = git_file_status - st.git_is_dirty = git_is_dirty and "*" or "" - st.git_status_str = git_status_str - st.is_ignore_dir = is_ignore_dir - st.is_untracked_dir= is_untracked_dir - ya.render() +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 -end) -local clear_state = ya.sync(function(st) - st.git_branch = "" - st.git_file_status = "" - st.git_is_dirty = "" + 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 k, v in pairs(changed) do + if v ~= 3 and v ~= 30 then + local url = Url(k):parent() + while url and url ~= empty do + local s = tostring(url) + new[s] = (new[s] or 0) > v and new[s] or v + url = url:parent() + end + end + end + return new +end + +local function propagate_down(ignored, cwd, repo) + local new, rel = {}, cwd:strip_prefix(repo) + for k, v in pairs(ignored) do + if v == 30 then + if rel:starts_with(k) then + new[tostring(repo:join(rel))] = 30 + elseif cwd == repo:join(k):parent() then + new[k] = 3 + end + 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 k, v in pairs(changed) do + if v == 0 then + st.repos[repo][k] = nil + elseif v == 30 then + st.dirs[k] = "" + else + st.repos[repo][k] = v + end + end ya.render() end) -local function update_git_status(path) - ya.manager_emit("plugin", { "git", args = ya.quote(tostring(path))}) -end - -local is_in_git_dir = ya.sync(function(st) - return (st.git_branch ~= nil and st.git_branch ~= "") and cx.active.current.cwd or nil -end) - -local flush_empty_folder_status = ya.sync(function(st) - local cwd = cx.active.current.cwd - local folder = cx.active.current - if #folder.window == 0 then - clear_state() - ya.manager_emit("plugin", { "git", args = ya.quote(tostring(cwd))}) +local remove = ya.sync(function(st, cwd) + local dir = st.dirs[cwd] + if not dir then + return end -end) -local handle_path_change = ya.sync(function(st) - local cwd = cx.active.current.cwd - if st.cwd ~= cwd then - st.cwd = cwd - clear_state() - ya.manager_emit("plugin", { "git", args = ya.quote(tostring(cwd))}) + ya.render() + st.dirs[cwd] = nil + if not st.repos[dir] then + return end -end) - -local M = { - setup = function(st,opts) - - local function linemode_git(self) - local f = self._file - local git_span = {} - local git_status - if st.git_branch ~= nil and st.git_branch ~= "" then - local name = f.name:gsub("\r", "?", 1) - if st.is_ignore_dir then - git_status = "." - elseif st.is_untracked_dir then - git_status = "?" - elseif st.git_file_status and st.git_file_status[name] then - git_status = st.git_file_status[name] - else - git_status = nil - end - - local color = set_status_color(git_status) - if f:is_hovered() then - git_span = (git_status ) and ui.Span(git_status .." ") or ui.Span("✓ ") - else - git_span = (git_status) and ui.Span(git_status .." "):fg(color) or ui.Span("✓ "):fg(color) - end - end - return git_span - end - Linemode:children_add(linemode_git,8000) - - ps.sub("cd",handle_path_change) - ps.sub("delete",flush_empty_folder_status) - ps.sub("trash",flush_empty_folder_status) - end, - - entry = function(_, args) - local output - local git_is_dirty - local is_ignore_dir,is_untracked_dir - - local git_branch - local command = "git symbolic-ref HEAD 2> /dev/null" - local file = io.popen(command, "r") - output = file:read("*a") - file:close() - - if output ~= nil and output ~= "" then - local split_output = string_split(output:sub(1,-2),"/") - - git_branch = split_output[3] - elseif is_in_git_dir() then - git_branch = nil - else + for _, r in pairs(st.dirs) do + if r == dir then return end - - local git_status_str = "" - local git_file_status = nil - local command = "git status --ignored -s --ignore-submodules=dirty 2> /dev/null" - local file = io.popen(command, "r") - output = file:read("*a") - file:close() - - if output ~= nil and output ~= "" then - git_status_str = output - git_file_status,git_is_dirty,is_ignore_dir,is_untracked_dir = make_git_table(git_status_str) - end - save(args[1], git_branch,git_file_status,git_is_dirty,git_status_str,is_ignore_dir,is_untracked_dir) - end, -} - -function M:fetch() - local path = is_in_git_dir() - if path then - update_git_status(path) end + st.repos[dir] = nil +end) + +local function setup(st, opts) + st.dirs = {} + st.repos = {} + + opts = opts or {} + opts.order = opts.order or 1500 + + -- Chosen by ChatGPT fairly, PRs are welcome to adjust them + local t = THEME.git or {} + local styles = { + [6] = t.modified and ui.Style(t.modified) or ui.Style():fg("#ffa500"), + [5] = t.added and ui.Style(t.added) or ui.Style():fg("#32cd32"), + [4] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("#a9a9a9"), + [3] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("#696969"), + [2] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("#ff4500"), + [1] = t.updated and ui.Style(t.updated) or ui.Style():fg("#1e90ff"), + } + local signs = { + [6] = t.modified_sign and t.modified_sign or "", + [5] = t.added_sign and t.added_sign or "", + [4] = t.untracked_sign and t.untracked_sign or "", + [3] = t.ignored_sign and t.ignored_sign or "", + [2] = t.deleted_sign and t.deleted_sign or "", + [1] = t.updated_sign and t.updated_sign or "U", + } + + Linemode:children_add(function(self) + local url = self._file.url + local dir = st.dirs[tostring(url:parent())] + local change + if dir then + change = dir == "" and 3 or st.repos[dir][tostring(url):sub(#dir + 2)] + end + + if not change or signs[change] == "" then + return ui.Line("") + elseif self._file:is_hovered() then + return ui.Line { ui.Span(" "), ui.Span(signs[change]) } + else + return ui.Line { ui.Span(" "), ui.Span(signs[change]):style(styles[change]) } + end + end, opts.order) +end + +local function fetch(self, job) + -- TODO: remove this once Yazi 0.4 is released + job = job or self + + local cwd = job.files[1].url:parent() + local repo = root(cwd) + if not repo then + remove(tostring(cwd)) + return 1 + end + + local paths = {} + for _, f in ipairs(job.files) do + paths[#paths + 1] = tostring(f.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 + ya.err("Cannot spawn git command, error code " .. tostring(err)) + return 0 + end + + local changed, ignored = {}, {} + for line in output.stdout:gmatch("[^\r\n]+") do + local sign, path = match(line) + if sign == 30 then + ignored[path] = sign + else + changed[path] = sign + end + end + + if job.files[1].cha.is_dir then + ya.dict_merge(changed, bubble_up(changed)) + ya.dict_merge(changed, propagate_down(ignored, cwd, Url(repo))) + else + ya.dict_merge(changed, propagate_down(ignored, cwd, Url(repo))) + end + + for _, p in ipairs(paths) do + local s = p:sub(#repo + 2) + changed[s] = changed[s] or 0 + end + add(tostring(cwd), repo, changed) + return 3 end -return M \ No newline at end of file +return { setup = setup, fetch = fetch } diff --git a/yazi/plugins/smart-enter.yazi/init.lua b/yazi/plugins/smart-enter.yazi/init.lua index efa1070..37a465a 100644 --- a/yazi/plugins/smart-enter.yazi/init.lua +++ b/yazi/plugins/smart-enter.yazi/init.lua @@ -1,6 +1,10 @@ -return { - entry = function() - local h = cx.active.current.hovered - ya.manager_emit(h and h.cha.is_dir and "enter" or "open", { hovered = true }) - end, -} +--- @sync entry + +local function setup(self, opts) self.open_multi = opts.open_multi end + +local function entry(self) + local h = cx.active.current.hovered + ya.manager_emit(h and h.cha.is_dir and "enter" or "open", { hovered = not self.open_multi }) +end + +return { entry = entry, setup = setup } diff --git a/yazi/plugins/starship.yazi/README.md b/yazi/plugins/starship.yazi/README.md index 39b632d..a6fcab4 100644 --- a/yazi/plugins/starship.yazi/README.md +++ b/yazi/plugins/starship.yazi/README.md @@ -6,10 +6,10 @@ Starship prompt plugin for [Yazi](https://github.com/sxyazi/yazi) ## Requirements -- [Yazi](https://github.com/sxyazi/yazi) - latest main branch +- [Yazi](https://github.com/sxyazi/yazi) - [starship](https://github.com/starship/starship) -### Package manager +## Installation ```bash ya pack -a Rolv-Apneseth/starship @@ -17,15 +17,10 @@ ya pack -a Rolv-Apneseth/starship ### Manual -#### Linux / MacOS - ```sh +# Linux / MacOS git clone https://github.com/Rolv-Apneseth/starship.yazi.git ~/.config/yazi/plugins/starship.yazi -``` - -#### Windows - -```sh +# Windows git clone https://github.com/Rolv-Apneseth/starship.yazi.git %AppData%\yazi\config\plugins\starship.yazi ``` diff --git a/yazi/plugins/starship.yazi/init.lua b/yazi/plugins/starship.yazi/init.lua index b8672c5..292e75e 100644 --- a/yazi/plugins/starship.yazi/init.lua +++ b/yazi/plugins/starship.yazi/init.lua @@ -61,7 +61,12 @@ return { ps.sub("tab", callback) end, - entry = function(_, args) + entry = function(_, job_or_args) + -- yazi 2024-11-29 changed the way arguments are passed to the plugin + -- entry point. They were moved inside {args = {...}}. If the user is using + -- a version before this change, they can use the old implementation. + -- https://github.com/sxyazi/yazi/pull/1966 + local args = job_or_args.args or job_or_args local command = Command("starship"):arg("prompt"):cwd(args[1]):env("STARSHIP_SHELL", "") -- Point to custom starship config