Compare commits

...

2 commits

Author SHA1 Message Date
189e89212d
Update nvim submodule 2025-04-10 15:29:47 +02:00
09065ab756
Update yazi and its plugins 2025-04-10 15:28:26 +02:00
17 changed files with 1806 additions and 114 deletions

@ -1 +1 @@
Subproject commit caad93bd07c2ba025a1a03d9e58429046306925f Subproject commit d4769876253834a269cd2dd324fbb7436a9a4daf

View file

@ -152,7 +152,7 @@ keymap = [
{ on = "}", run = "tab_swap 1", desc = "Swap current tab with next tab" }, { on = "}", run = "tab_swap 1", desc = "Swap current tab with next tab" },
# Tasks # Tasks
{ on = "w", run = "tasks_show", desc = "Show task manager" }, { on = "w", run = "tasks:show", desc = "Show task manager" },
# Help # Help
{ on = "~", run = "help", desc = "Open help" }, { on = "~", run = "help", desc = "Open help" },
@ -164,6 +164,15 @@ on = [ "g", "i" ]
run = "plugin lazygit" run = "plugin lazygit"
desc = "run lazygit" desc = "run lazygit"
[[manager.prepend_keymap]]
on = "M"
run = "plugin mount"
[[manager.prepend_keymap]]
on = [ "g", "c" ]
run = "plugin vcs-files"
desc = "Show Git file changes"
[tasks] [tasks]
keymap = [ keymap = [
@ -328,7 +337,7 @@ keymap = [
keymap = [ keymap = [
{ on = "<C-c>", run = "close", desc = "Cancel completion" }, { on = "<C-c>", run = "close", desc = "Cancel completion" },
{ on = "<Tab>", run = "close --submit", desc = "Submit the completion" }, { on = "<Tab>", run = "close --submit", desc = "Submit the completion" },
{ on = "<Enter>", run = [ "close --submit", "close_input --submit" ], desc = "Submit the completion and input" }, { on = "<Enter>", run = [ "close --submit", "input:close --submit" ], desc = "Submit the completion and input" },
{ on = "<A-k>", run = "arrow -1", desc = "Move cursor up" }, { on = "<A-k>", run = "arrow -1", desc = "Move cursor up" },
{ on = "<A-j>", run = "arrow 1", desc = "Move cursor down" }, { on = "<A-j>", run = "arrow 1", desc = "Move cursor down" },

View file

@ -1,17 +1,32 @@
[[plugin.deps]] [[plugin.deps]]
use = "yazi-rs/plugins:git" use = "yazi-rs/plugins:git"
rev = "5186af7" rev = "a1738e8"
hash = "771f18427fb75fb19990ce602bb322f4" hash = "e0ada736ea676c2bbb3ec705a49526ef"
[[plugin.deps]] [[plugin.deps]]
use = "yazi-rs/plugins:full-border" use = "yazi-rs/plugins:full-border"
rev = "5186af7" rev = "a1738e8"
hash = "ae9e1d0c6bfd68cdebc98cc684c22b45" hash = "1f3dad061209081a6b04dd6ff2cb06c7"
[[plugin.deps]] [[plugin.deps]]
use = "Lil-Dank/lazygit" use = "Lil-Dank/lazygit"
rev = "9f924e3" rev = "7a08a09"
hash = "2b0c33928986f126d5c5a4cac31a6b46" hash = "a1fa2b3e1826c3a34804ea8c548e9f80"
[[plugin.deps]]
use = "yazi-rs/plugins:mime-ext"
rev = "a1738e8"
hash = "5e24c167e9ae9a203a48f2a438b8c705"
[[plugin.deps]]
use = "yazi-rs/plugins:mount"
rev = "a1738e8"
hash = "dd97eede8e20e59cd2604e8006e470e2"
[[plugin.deps]]
use = "yazi-rs/plugins:vcs-files"
rev = "a1738e8"
hash = "36405f8781fcaf33ceab24dffd5cea19"
[flavor] [flavor]
deps = [] deps = []

View file

@ -1,4 +1,4 @@
--- @since 25.2.7 --- @since 25.2.26
local function setup(_, opts) local function setup(_, opts)
local type = opts and opts.type or ui.Border.ROUNDED local type = opts and opts.type or ui.Border.ROUNDED
@ -6,7 +6,7 @@ local function setup(_, opts)
Tab.build = function(self, ...) Tab.build = function(self, ...)
local bar = function(c, x, y) local bar = function(c, x, y)
if x <= 0 or x == self._area.w - 1 then if x <= 0 or x == self._area.w - 1 or th.mgr.border_symbol ~= "" then
return ui.Bar(ui.Bar.TOP) return ui.Bar(ui.Bar.TOP)
end end
@ -24,7 +24,7 @@ local function setup(_, opts)
c[3]:pad(ui.Pad.y(1)), c[3]:pad(ui.Pad.y(1)),
} }
local style = THEME.manager.border_style local style = th.mgr.border_style
self._base = ya.list_merge(self._base or {}, { self._base = ya.list_merge(self._base or {}, {
ui.Border(ui.Border.ALL):area(self._area):type(type):style(style), ui.Border(ui.Border.ALL):area(self._area):type(type):style(style),
ui.Bar(ui.Bar.RIGHT):area(self._chunks[1]):style(style), ui.Bar(ui.Bar.RIGHT):area(self._chunks[1]):style(style),

View file

@ -1,7 +1,7 @@
# git.yazi # git.yazi
> [!NOTE] > [!NOTE]
> Yazi v25.2.7 or later is required for this plugin to work. > 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. Show the status of Git file changes as linemode in the file list.
@ -39,38 +39,38 @@ run = "git"
You can customize the [Style](https://yazi-rs.github.io/docs/plugins/layout#style) of the status sign with: You can customize the [Style](https://yazi-rs.github.io/docs/plugins/layout#style) of the status sign with:
- `THEME.git.modified` - `th.git.modified`
- `THEME.git.added` - `th.git.added`
- `THEME.git.untracked` - `th.git.untracked`
- `THEME.git.ignored` - `th.git.ignored`
- `THEME.git.deleted` - `th.git.deleted`
- `THEME.git.updated` - `th.git.updated`
For example: For example:
```lua ```lua
-- ~/.config/yazi/init.lua -- ~/.config/yazi/init.lua
THEME.git = THEME.git or {} th.git = th.git or {}
THEME.git.modified = ui.Style():fg("blue") th.git.modified = ui.Style():fg("blue")
THEME.git.deleted = ui.Style():fg("red"):bold() th.git.deleted = ui.Style():fg("red"):bold()
``` ```
You can also customize the text of the status sign with: You can also customize the text of the status sign with:
- `THEME.git.modified_sign` - `th.git.modified_sign`
- `THEME.git.added_sign` - `th.git.added_sign`
- `THEME.git.untracked_sign` - `th.git.untracked_sign`
- `THEME.git.ignored_sign` - `th.git.ignored_sign`
- `THEME.git.deleted_sign` - `th.git.deleted_sign`
- `THEME.git.updated_sign` - `th.git.updated_sign`
For example: For example:
```lua ```lua
-- ~/.config/yazi/init.lua -- ~/.config/yazi/init.lua
THEME.git = THEME.git or {} th.git = th.git or {}
THEME.git.modified_sign = "M" th.git.modified_sign = "M"
THEME.git.deleted_sign = "D" th.git.deleted_sign = "D"
``` ```
## License ## License

View file

@ -1,29 +1,45 @@
--- @since 25.2.7 --- @since 25.4.4
local WIN = ya.target_family() == "windows" local WINDOWS = ya.target_family() == "windows"
local PATS = {
{ "[MT]", 6 }, -- Modified -- The code of supported git status,
{ "[AC]", 5 }, -- Added -- also used to determine which status to show for directories when they contain different statuses
{ "?$", 4 }, -- Untracked -- see `bubble_up`
{ "!$", 3 }, -- Ignored local CODES = {
{ "D", 2 }, -- Deleted excluded = 100, -- ignored directory
{ "U", 1 }, -- Updated ignored = 6, -- ignored file
{ "[AD][AD]", 1 }, -- Updated 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 function match(line)
local signs = line:sub(1, 2) local signs = line:sub(1, 2)
for _, p in ipairs(PATS) do for _, p in ipairs(PATTERNS) do
local path local path, pattern, code = nil, p[1], p[2]
if signs:find(p[1]) then if signs:find(pattern) then
path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4) path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4)
path = WIN and path:gsub("/", "\\") or path path = WINDOWS and path:gsub("/", "\\") or path
end end
if not path then if not path then
elseif path:find("[/\\]$") then elseif path:find("[/\\]$") then
return p[2] == 3 and 30 or p[2], path:sub(1, -2) -- 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 else
return p[2], path return code, path
end end
end end
end end
@ -44,34 +60,36 @@ local function root(cwd)
if cha and (cha.is_dir or is_worktree(next)) then if cha and (cha.is_dir or is_worktree(next)) then
return tostring(cwd) return tostring(cwd)
end end
cwd = cwd:parent() cwd = cwd.parent
until not cwd until not cwd
end end
local function bubble_up(changed) local function bubble_up(changed)
local new, empty = {}, Url("") local new, empty = {}, Url("")
for k, v in pairs(changed) do for path, code in pairs(changed) do
if v ~= 3 and v ~= 30 then if code ~= CODES.ignored then
local url = Url(k):parent() local url = Url(path).parent
while url and url ~= empty do while url and url ~= empty do
local s = tostring(url) local s = tostring(url)
new[s] = (new[s] or 0) > v and new[s] or v new[s] = (new[s] or CODES.unknown) > code and new[s] or code
url = url:parent() url = url.parent
end end
end end
end end
return new return new
end end
local function propagate_down(ignored, cwd, repo) local function propagate_down(excluded, cwd, repo)
local new, rel = {}, cwd:strip_prefix(repo) local new, rel = {}, cwd:strip_prefix(repo)
for k, v in pairs(ignored) do for _, path in ipairs(excluded) do
if v == 30 then if rel:starts_with(path) then
if rel:starts_with(k) then -- If `cwd` is a subdirectory of an excluded directory, also mark it as `excluded`
new[tostring(repo:join(rel))] = 30 new[tostring(cwd)] = CODES.excluded
elseif cwd == repo:join(k):parent() then elseif cwd == repo:join(path).parent then
new[k] = 3 -- If `path` is a direct subdirectory of `cwd`, mark it as `ignored`
end new[path] = CODES.ignored
else
-- Skipping, we only care about `cwd` itself and its direct subdirectories for maximum performance
end end
end end
return new return new
@ -80,84 +98,84 @@ end
local add = ya.sync(function(st, cwd, repo, changed) local add = ya.sync(function(st, cwd, repo, changed)
st.dirs[cwd] = repo st.dirs[cwd] = repo
st.repos[repo] = st.repos[repo] or {} st.repos[repo] = st.repos[repo] or {}
for k, v in pairs(changed) do for path, code in pairs(changed) do
if v == 0 then if code == CODES.unknown then
st.repos[repo][k] = nil st.repos[repo][path] = nil
elseif v == 30 then elseif code == CODES.excluded then
st.dirs[k] = "" -- Mark the directory with a special value `excluded` so that it can be distinguished during UI rendering
st.dirs[path] = CODES.excluded
else else
st.repos[repo][k] = v st.repos[repo][path] = code
end end
end end
ya.render() ya.render()
end) end)
local remove = ya.sync(function(st, cwd) local remove = ya.sync(function(st, cwd)
local dir = st.dirs[cwd] local repo = st.dirs[cwd]
if not dir then if not repo then
return return
end end
ya.render() ya.render()
st.dirs[cwd] = nil st.dirs[cwd] = nil
if not st.repos[dir] then if not st.repos[repo] then
return return
end end
for _, r in pairs(st.dirs) do for _, r in pairs(st.dirs) do
if r == dir then if r == repo then
return return
end end
end end
st.repos[dir] = nil st.repos[repo] = nil
end) end)
local function setup(st, opts) local function setup(st, opts)
st.dirs = {} st.dirs = {} -- Mapping between a directory and its corresponding repository
st.repos = {} st.repos = {} -- Mapping between a repository and the status of each of its files
opts = opts or {} opts = opts or {}
opts.order = opts.order or 1500 opts.order = opts.order or 1500
-- Chosen by ChatGPT fairly, PRs are welcome to adjust them local t = th.git or {}
local t = THEME.git or {}
local styles = { local styles = {
[6] = t.modified and ui.Style(t.modified) or ui.Style():fg("#ffa500"), [CODES.ignored] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("darkgray"),
[5] = t.added and ui.Style(t.added) or ui.Style():fg("#32cd32"), [CODES.untracked] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("magenta"),
[4] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("#a9a9a9"), [CODES.modified] = t.modified and ui.Style(t.modified) or ui.Style():fg("yellow"),
[3] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("#696969"), [CODES.added] = t.added and ui.Style(t.added) or ui.Style():fg("green"),
[2] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("#ff4500"), [CODES.deleted] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("red"),
[1] = t.updated and ui.Style(t.updated) or ui.Style():fg("#1e90ff"), [CODES.updated] = t.updated and ui.Style(t.updated) or ui.Style():fg("yellow"),
} }
local signs = { local signs = {
[6] = t.modified_sign and t.modified_sign or "", [CODES.ignored] = t.ignored_sign or "",
[5] = t.added_sign and t.added_sign or "", [CODES.untracked] = t.untracked_sign or "?",
[4] = t.untracked_sign and t.untracked_sign or "", [CODES.modified] = t.modified_sign or "",
[3] = t.ignored_sign and t.ignored_sign or "", [CODES.added] = t.added_sign or "",
[2] = t.deleted_sign and t.deleted_sign or "", [CODES.deleted] = t.deleted_sign or "",
[1] = t.updated_sign and t.updated_sign or "U", [CODES.updated] = t.updated_sign or "",
} }
Linemode:children_add(function(self) Linemode:children_add(function(self)
local url = self._file.url local url = self._file.url
local dir = st.dirs[tostring(url:parent())] local repo = st.dirs[tostring(url.base)]
local change local code
if dir then if repo then
change = dir == "" and 3 or st.repos[dir][tostring(url):sub(#dir + 2)] code = repo == CODES.excluded and CODES.ignored or st.repos[repo][tostring(url):sub(#repo + 2)]
end end
if not change or signs[change] == "" then if not code or signs[code] == "" then
return "" return ""
elseif self._file:is_hovered() then elseif self._file.is_hovered then
return ui.Line { " ", signs[change] } return ui.Line { " ", signs[code] }
else else
return ui.Line { " ", ui.Span(signs[change]):style(styles[change]) } return ui.Line { " ", ui.Span(signs[code]):style(styles[code]) }
end end
end, opts.order) end, opts.order)
end end
local function fetch(_, job) local function fetch(_, job)
local cwd = job.files[1].url:parent() local cwd = job.files[1].url.base
local repo = root(cwd) local repo = root(cwd)
if not repo then if not repo then
remove(tostring(cwd)) remove(tostring(cwd))
@ -165,8 +183,8 @@ local function fetch(_, job)
end end
local paths = {} local paths = {}
for _, f in ipairs(job.files) do for _, file in ipairs(job.files) do
paths[#paths + 1] = tostring(f.url) paths[#paths + 1] = tostring(file.url)
end end
-- stylua: ignore -- stylua: ignore
@ -180,27 +198,28 @@ local function fetch(_, job)
return true, Err("Cannot spawn `git` command, error: %s", err) return true, Err("Cannot spawn `git` command, error: %s", err)
end end
local changed, ignored = {}, {} local changed, excluded = {}, {}
for line in output.stdout:gmatch("[^\r\n]+") do for line in output.stdout:gmatch("[^\r\n]+") do
local sign, path = match(line) local code, path = match(line)
if sign == 30 then if code == CODES.excluded then
ignored[path] = sign excluded[#excluded + 1] = path
else else
changed[path] = sign changed[path] = code
end end
end end
if job.files[1].cha.is_dir then if job.files[1].cha.is_dir then
ya.dict_merge(changed, bubble_up(changed)) ya.dict_merge(changed, bubble_up(changed))
ya.dict_merge(changed, propagate_down(ignored, cwd, Url(repo))) end
else ya.dict_merge(changed, propagate_down(excluded, cwd, Url(repo)))
ya.dict_merge(changed, propagate_down(ignored, 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 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) add(tostring(cwd), repo, changed)
return false return false

View file

@ -4,7 +4,7 @@ return {
if output.stderr ~= "" then if output.stderr ~= "" then
ya.notify({ ya.notify({
title = "lazygit", title = "lazygit",
content = "Not in a git directory", content = "Not in a git directory\nError: " .. output.stderr,
level = "warn", level = "warn",
timeout = 5, timeout = 5,
}) })
@ -28,4 +28,4 @@ return {
end end
end end
end, end,
} }

View file

@ -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.

View file

@ -0,0 +1,56 @@
# mime-ext.yazi
A mime-type provider based on a file extension database, replacing the [builtin `file(1)`](https://github.com/sxyazi/yazi/blob/main/yazi-plugin/preset/plugins/mime.lua) to speed up mime-type retrieval at the expense of accuracy.
See https://yazi-rs.github.io/docs/tips#make-yazi-even-faster for more information.
## Installation
```sh
ya pack -a yazi-rs/plugins:mime-ext
```
## Usage
Add this to your `~/.config/yazi/yazi.toml`:
```toml
[[plugin.prepend_fetchers]]
id = "mime"
name = "*"
run = "mime-ext"
prio = "high"
```
## Advanced
You can also customize it in your `~/.config/yazi/init.lua` with:
```lua
require("mime-ext"):setup {
-- Expand the existing filename database (lowercase), for example:
with_files = {
makefile = "text/makefile",
-- ...
},
-- Expand the existing extension database (lowercase), for example:
with_exts = {
mk = "text/makefile",
-- ...
},
-- If the mime-type is not in both filename and extension databases,
-- then fallback to Yazi's preset `mime` plugin, which uses `file(1)`
fallback_file1 = false,
}
```
## TODO
- Add more file types (PRs welcome!).
- Compress mime-type tables.
## License
This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file.

File diff suppressed because it is too large Load diff

View file

@ -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.

View file

@ -0,0 +1,51 @@
# mount.yazi
> [!NOTE]
> Yazi v25.2.7 or later is required for this plugin to work.
A mount manager for Yazi, providing disk mount, unmount, and eject functionality.
Supported platforms:
- Linux with [`udisksctl`](https://github.com/storaged-project/udisks) and [`lsblk`](https://github.com/util-linux/util-linux)
- macOS with `diskutil`
https://github.com/user-attachments/assets/c6f780ab-458b-420f-85cf-2fc45fcfe3a2
## Installation
```sh
ya pack -a yazi-rs/plugins:mount
```
## Usage
Add this to your `~/.config/yazi/keymap.toml`:
```toml
[[manager.prepend_keymap]]
on = "M"
run = "plugin mount"
```
Available keybindings:
| Key binding | Alternate key | Action |
| ------------ | ------------- | --------------------- |
| <kbd>q</kbd> | - | Quit the plugin |
| <kbd>k</kbd> | <kbd></kbd> | Move up |
| <kbd>j</kbd> | <kbd></kbd> | Move down |
| <kbd>l</kbd> | <kbd></kbd> | Enter the mount point |
| <kbd>m</kbd> | - | Mount the partition |
| <kbd>u</kbd> | - | Unmount the partition |
| <kbd>e</kbd> | - | Eject the disk |
## TODO
- Custom keybindings
- Windows support (I don't have an Windows machine for testing, PRs welcome!)
- Support mount, unmount, and eject the entire disk
## License
This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file.

View file

@ -0,0 +1,285 @@
--- @since 25.2.26
local toggle_ui = ya.sync(function(self)
if self.children then
Modal:children_remove(self.children)
self.children = nil
else
self.children = Modal:children_add(self, 10)
end
ya.render()
end)
local subscribe = ya.sync(function(self)
ps.unsub("mount")
ps.sub("mount", function() ya.mgr_emit("plugin", { self._id, "refresh" }) end)
end)
local update_partitions = ya.sync(function(self, partitions)
self.partitions = partitions
self.cursor = math.max(0, math.min(self.cursor or 0, #self.partitions - 1))
ya.render()
end)
local active_partition = ya.sync(function(self) return self.partitions[self.cursor + 1] end)
local update_cursor = ya.sync(function(self, cursor)
if #self.partitions == 0 then
self.cursor = 0
else
self.cursor = ya.clamp(0, self.cursor + cursor, #self.partitions - 1)
end
ya.render()
end)
local M = {
keys = {
{ on = "q", run = "quit" },
{ on = "k", run = "up" },
{ on = "j", run = "down" },
{ on = "l", run = { "enter", "quit" } },
{ on = "<Up>", run = "up" },
{ on = "<Down>", run = "down" },
{ on = "<Right>", run = { "enter", "quit" } },
{ on = "m", run = "mount" },
{ on = "u", run = "unmount" },
{ on = "e", run = "eject" },
},
}
function M:new(area)
self:layout(area)
return self
end
function M:layout(area)
local chunks = ui.Layout()
:constraints({
ui.Constraint.Percentage(10),
ui.Constraint.Percentage(80),
ui.Constraint.Percentage(10),
})
:split(area)
local chunks = ui.Layout()
:direction(ui.Layout.HORIZONTAL)
:constraints({
ui.Constraint.Percentage(10),
ui.Constraint.Percentage(80),
ui.Constraint.Percentage(10),
})
:split(chunks[2])
self._area = chunks[2]
end
function M:entry(job)
if job.args[1] == "refresh" then
return update_partitions(self.obtain())
end
toggle_ui()
update_partitions(self.obtain())
subscribe()
local tx1, rx1 = ya.chan("mpsc")
local tx2, rx2 = ya.chan("mpsc")
function producer()
while true do
local cand = self.keys[ya.which { cands = self.keys, silent = true }] or { run = {} }
for _, r in ipairs(type(cand.run) == "table" and cand.run or { cand.run }) do
tx1:send(r)
if r == "quit" then
toggle_ui()
return
end
end
end
end
function consumer1()
repeat
local run = rx1:recv()
if run == "quit" then
tx2:send(run)
break
elseif run == "up" then
update_cursor(-1)
elseif run == "down" then
update_cursor(1)
elseif run == "enter" then
local active = active_partition()
if active and active.dist then
ya.mgr_emit("cd", { active.dist })
end
else
tx2:send(run)
end
until not run
end
function consumer2()
repeat
local run = rx2:recv()
if run == "quit" then
break
elseif run == "mount" then
self.operate("mount")
elseif run == "unmount" then
self.operate("unmount")
elseif run == "eject" then
self.operate("eject")
end
until not run
end
ya.join(producer, consumer1, consumer2)
end
function M:reflow() return { self } end
function M:redraw()
local rows = {}
for _, p in ipairs(self.partitions or {}) do
if not p.sub then
rows[#rows + 1] = ui.Row { p.main }
elseif p.sub == "" then
rows[#rows + 1] = ui.Row { p.main, p.label or "", p.dist or "", p.fstype or "" }
else
rows[#rows + 1] = ui.Row { " " .. p.sub, p.label or "", p.dist or "", p.fstype or "" }
end
end
return {
ui.Clear(self._area),
ui.Border(ui.Border.ALL)
:area(self._area)
:type(ui.Border.ROUNDED)
:style(ui.Style():fg("blue"))
:title(ui.Line("Mount"):align(ui.Line.CENTER)),
ui.Table(rows)
:area(self._area:pad(ui.Pad(1, 2, 1, 2)))
:header(ui.Row({ "Src", "Label", "Dist", "FSType" }):style(ui.Style():bold()))
:row(self.cursor)
:row_style(ui.Style():fg("blue"):underline())
:widths {
ui.Constraint.Length(20),
ui.Constraint.Length(20),
ui.Constraint.Percentage(70),
ui.Constraint.Length(10),
},
}
end
function M.obtain()
local tbl = {}
local last
for _, p in ipairs(fs.partitions()) do
local main, sub = M.split(p.src)
if main and last ~= main then
if p.src == main then
last, p.main, p.sub, tbl[#tbl + 1] = p.src, p.src, "", p
else
last, tbl[#tbl + 1] = main, { src = main, main = main, sub = "" }
end
end
if sub then
if tbl[#tbl].sub == "" and tbl[#tbl].main == main then
tbl[#tbl].sub = nil
end
p.main, p.sub, tbl[#tbl + 1] = main, sub, p
end
end
table.sort(M.fillin(tbl), function(a, b)
if a.main == b.main then
return (a.sub or "") < (b.sub or "")
else
return a.main > b.main
end
end)
return tbl
end
function M.split(src)
local pats = {
{ "^/dev/sd[a-z]", "%d+$" }, -- /dev/sda1
{ "^/dev/nvme%d+n%d+", "p%d+$" }, -- /dev/nvme0n1p1
{ "^/dev/mmcblk%d+", "p%d+$" }, -- /dev/mmcblk0p1
{ "^/dev/disk%d+", ".+$" }, -- /dev/disk1s1
}
for _, p in ipairs(pats) do
local main = src:match(p[1])
if main then
return main, src:sub(#main + 1):match(p[2])
end
end
end
function M.fillin(tbl)
if ya.target_os() ~= "linux" then
return tbl
end
local sources, indices = {}, {}
for i, p in ipairs(tbl) do
if p.sub and not p.fstype then
sources[#sources + 1], indices[p.src] = p.src, i
end
end
if #sources == 0 then
return tbl
end
local output, err = Command("lsblk"):args({ "-p", "-o", "name,fstype", "-J" }):args(sources):output()
if err then
ya.dbg("Failed to fetch filesystem types for unmounted partitions: " .. err)
return tbl
end
local t = ya.json_decode(output and output.stdout or "")
for _, p in ipairs(t and t.blockdevices or {}) do
tbl[indices[p.name]].fstype = p.fstype
end
return tbl
end
function M.operate(type)
local active = active_partition()
if not active then
return
elseif not active.sub then
return -- TODO: mount/unmount main disk
end
local output, err
if ya.target_os() == "macos" then
output, err = Command("diskutil"):args({ type, active.src }):output()
end
if ya.target_os() == "linux" then
if type == "eject" then
Command("udisksctl"):args({ "unmount", "-b", active.src }):status()
output, err = Command("udisksctl"):args({ "power-off", "-b", active.src }):output()
else
output, err = Command("udisksctl"):args({ type, "-b", active.src }):output()
end
end
if not output then
M.fail("Failed to %s `%s`: %s", type, active.src, err)
elseif not output.status.success then
M.fail("Failed to %s `%s`: %s", type, active.src, output.stderr)
end
end
function M.fail(s, ...) ya.notify { title = "Mount", content = string.format(s, ...), timeout = 10, level = "error" } end
function M:click() end
function M:scroll() end
function M:touch() end
return M

View file

@ -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.

View file

@ -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.

View file

@ -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 }

View file

@ -82,7 +82,7 @@ macro_workers = 25
bizarre_retry = 5 bizarre_retry = 5
image_alloc = 536870912 # 512MB image_alloc = 536870912 # 512MB
image_bound = [ 0, 0 ] image_bound = [ 0, 0 ]
suppress_preload = false suppress_preload = true
[plugin] [plugin]
@ -156,6 +156,12 @@ id = "git"
name = "*/" name = "*/"
run = "git" run = "git"
[[plugin.prepend_fetchers]]
id = "mime"
name = "*"
run = "mime-ext"
prio = "high"
[input] [input]
cursor_blink = false cursor_blink = false