diff --git a/.config/fsel/config.toml b/.config/fsel/config.toml new file mode 100644 index 0000000..5639e7b --- /dev/null +++ b/.config/fsel/config.toml @@ -0,0 +1,22 @@ +# Root level - UI/Color options go here +highlight_color = "LightBlue" +main_border_color = "White" +pin_color = "Orange" +terminal_launcher = "kitty -e" # or "tty" for TTY mode (-t/--tty) + +# App launcher specific options +[app_launcher] +filter_desktop = true +filter_actions = false +list_executables_in_path = true +ranking_mode = "frecency" +pinned_order = "ranking" + +# Dmenu mode overrides +[dmenu] +delimiter = " " +show_line_numbers = true + +# Clipboard mode overrides +[cclip] +image_preview = true diff --git a/.config/i3/config b/.config/i3/config index aecef83..b0cb4de 100644 --- a/.config/i3/config +++ b/.config/i3/config @@ -146,9 +146,14 @@ bindsym $mod+Return exec kitty #bindsym Mod1+F2 exec dmenu_run #let's replace dmenu by custom launcher: -bindsym $mod+d exec ulauncher +# bindsym $mod+d exec ulauncher bindsym Mod1+F2 exec dmenu_run +# set $menu alacritty --title launcher -e fsel +bindsym $mod+d exec kitty --title launcher -o cursor_trail=0 -e fsel -r --detach +for_window [title="^launcher$"] floating enable, resize set width 500 height 430, border none + + # run app assigned to current workspace bindsym $mod+Shift+a exec $scriptsDir/autoapp ################################################################# diff --git a/.config/nvim/lazy-lock.json b/.config/nvim/lazy-lock.json index f292296..2dddbd2 100644 --- a/.config/nvim/lazy-lock.json +++ b/.config/nvim/lazy-lock.json @@ -1,10 +1,11 @@ { "LazyVim": { "branch": "main", "commit": "83d90f339defdb109a6ede333865a66ffc7ef6aa" }, - "avante.nvim": { "branch": "main", "commit": "f57f541959797bbca76e6e77efeb8b572e0bea2f" }, + "avante.nvim": { "branch": "main", "commit": "0e40cb0c0f13b87be8a2276a844428d60c466d78" }, "blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" }, "bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" }, "catppuccin": { "branch": "main", "commit": "8edd468af4d63212b84d69b2ddb5ffc9023ef5eb" }, - "conform.nvim": { "branch": "master", "commit": "18aeab3d63d350dcf44d64c462cc489a3412af40" }, + "clangd_extensions.nvim": { "branch": "main", "commit": "78c2ecd659d54972be17aa6ba2deac3c53223b80" }, + "conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" }, "diffview.nvim": { "branch": "main", "commit": "4516612fe98ff56ae0415a259ff6361a89419b0a" }, "dotenv.nvim": { "branch": "main", "commit": "7d516e9293c6e3ac21830fb10a4e8674c02747c6" }, "dressing.nvim": { "branch": "master", "commit": "2d7c2db2507fa3c4956142ee607431ddb2828639" }, @@ -26,16 +27,15 @@ "mini.pairs": { "branch": "main", "commit": "30cf2f01c4aaa2033db67376b9924fa2442c05d6" }, "noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" }, "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, - "nvim-dev-container": { "branch": "main", "commit": "365b4c815339d42133829006dce3e5f8b182b771" }, "nvim-lint": { "branch": "master", "commit": "d48f3a76189d03b2239f6df1b2f7e3fa8353743b" }, - "nvim-lspconfig": { "branch": "master", "commit": "a4ed4e761c400849e8c9f8bda33e5083f890268c" }, + "nvim-lspconfig": { "branch": "master", "commit": "9573948c38bfabeec353ae7dd7d3ffec4c506a6b" }, "nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" }, "nvim-treesitter-textobjects": { "branch": "main", "commit": "851e865342e5a4cb1ae23d31caf6e991e1c99f1e" }, "nvim-ts-autotag": { "branch": "main", "commit": "88c1453db4ba7dd24131086fe51fdf74e587d275" }, "persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" }, "plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" }, - "render-markdown.nvim": { "branch": "main", "commit": "0fd43fb4b1f073931c4b481f5f3b7cea3749e190" }, - "snacks.nvim": { "branch": "main", "commit": "0770753c88228f7f15449c6a5b242e3f7cd0d71c" }, + "render-markdown.nvim": { "branch": "main", "commit": "5adf0895310c1904e5abfaad40a2baad7fe44a07" }, + "snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" }, "todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" }, "tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" }, "trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" }, diff --git a/.config/yazi/flavors/everforest-medium.yazi/LICENSE b/.config/yazi/flavors/everforest-medium.yazi/LICENSE new file mode 100644 index 0000000..978c8ef --- /dev/null +++ b/.config/yazi/flavors/everforest-medium.yazi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Chromium Oxide + +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/.config/yazi/flavors/everforest-medium.yazi/LICENSE-tmtheme b/.config/yazi/flavors/everforest-medium.yazi/LICENSE-tmtheme new file mode 100644 index 0000000..020b42d --- /dev/null +++ b/.config/yazi/flavors/everforest-medium.yazi/LICENSE-tmtheme @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Mitchell Hanberg + +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/.config/yazi/flavors/everforest-medium.yazi/README.md b/.config/yazi/flavors/everforest-medium.yazi/README.md new file mode 100644 index 0000000..95029b5 --- /dev/null +++ b/.config/yazi/flavors/everforest-medium.yazi/README.md @@ -0,0 +1,45 @@ +
+ Yazi logo +
+ +

+ Example Flavor for Yazi +

+ +## 👀 Preview + + + +## 🎹 Installation + +```bash +ya pkg add Chromium-3-Oxide/everforest-medium +``` + +Or: + +```bash +ya pack -a Chromium-3-Oxide/everforest-medium +``` + +## ⚙ Usage + +Add the these lines to your `theme.toml` configuration file to use it: + +```toml +[flavor] +dark = "everforest-medium" +``` + +For Yazi versions before 0.4: + +```toml +[flavor] +use = "everforest-medium" +``` + +## 📜 License + +The flavor is MIT-licensed, and the included tmTheme is also MIT-licensed. + +Check the [LICENSE](LICENSE) and [LICENSE-tmtheme](LICENSE-tmtheme) file for more details. diff --git a/.config/yazi/flavors/everforest-medium.yazi/flavor.toml b/.config/yazi/flavors/everforest-medium.yazi/flavor.toml new file mode 100644 index 0000000..fb9a7c6 --- /dev/null +++ b/.config/yazi/flavors/everforest-medium.yazi/flavor.toml @@ -0,0 +1,179 @@ +# vim:fileencoding=utf-8:foldmethod=marker + +# : Manager {{{ + +[mgr] +cwd = { fg = "#7fbbb3" } + +# Find +find_keyword = { fg = "#dbbc7f", bold = true, italic = true, underline = true } +find_position = { fg = "#d699b6", bg = "reset", bold = true, italic = true } + +# Marker +marker_copied = { fg = "#a7c080", bg = "#a7c080" } +marker_cut = { fg = "#e67e80", bg = "#e67e80" } +marker_marked = { fg = "#7fbbb3", bg = "#7fbbb3" } +marker_selected = { fg = "#dbbc7f", bg = "#dbbc7f" } + +# Count +count_copied = { fg = "#343f44", bg = "#a7c080" } +count_cut = { fg = "#343f44", bg = "#e67e80" } +count_selected = { fg = "#343f44", bg = "#dbbc7f" } + +# Border +border_symbol = "│" +border_style = { fg = "#4f585e" } + +# : }}} + + +# : Tabs {{{ + +[tabs] +active = { fg = "#343f44", bg = "#a7c080", bold = true } +inactive = { fg = "#a7c080", bg = "#3d484d" } + +# : }}} + + +# : Mode {{{ + +[mode] + +normal_main = { fg = "#3d484d", bg = "#a7c080", bold = true } +normal_alt = { fg = "#7fbbb3", bg = "#4f585e", bold = true } + +# Select mode +select_main = { fg = "#3d484d", bg = "#e67e80", bold = true } +select_alt = { fg = "#7fbbb3", bg = "#4f585e", bold = true } + +# Unset mode +unset_main = { fg = "#3d484d", bg = "#7fbbb3", bold = true } +unset_alt = { fg = "#7fbbb3", bg = "#4f585e", bold = true } + +# : }}} + + +# : Status bar {{{ + +[status] +# Permissions +perm_sep = { fg = "#2d353b" } +perm_type = { fg = "#a7c080" } +perm_read = { fg = "#dbbc7f" } +perm_write = { fg = "#e67e80" } +perm_exec = { fg = "#7fbbb3" } + +# Progress +progress_label = { bold = true } +progress_normal = { fg = "#7fbbb3", bg = "#232a2e" } +progress_error = { fg = "#e67e80", bg = "#232a2e" } + +# : }}} + + +# : Pick {{{ + +[pick] +border = { fg = "#7fbbb3" } +active = { fg = "#d699b6", bold = true } +inactive = {} + +# : }}} + + +# : Input {{{ + +[input] +border = { fg = "#7fbbb3" } +title = {} +value = {} +selected = { reversed = true } + +# : }}} + + +# : Completion {{{ + +[cmp] +border = { fg = "#7fbbb3" } + +# : }}} + + +# : Tasks {{{ + +[tasks] +border = { fg = "#7fbbb3" } +title = {} +hovered = { fg = "#d699b6", underline = true } + +# : }}} + + +# : Which {{{ + +[which] +mask = { bg = "#2d353b" } +cand = { fg = "#7fbbb3" } +rest = { fg = "#2d353b" } +desc = { fg = "#d699b6" } +separator = " îȘœ " +separator_style = { fg = "#2d353b" } + +# : }}} + + +# : Help {{{ + +[help] +on = { fg = "#7fbbb3" } +run = { fg = "#d699b6" } +hovered = { reversed = true, bold = true } +footer = { fg = "#2d353b", bg = "#d3c6aa" } + +# : }}} + + +# : Spotter {{{ + +[spot] +border = { fg = "#7fbbb3" } +title = { fg = "#7fbbb3" } +tbl_col = { fg = "#7fbbb3" } +tbl_cell = { fg = "#dbbc7f", reversed = true } + +# : }}} + + +# : Notification {{{ + +[notify] +title_info = { fg = "#a7c080" } +title_warn = { fg = "#dbbc7f" } +title_error = { fg = "#e67e80" } + +# : }}} + + +# : File-specific styles {{{ + +[filetype] + +rules = [ + # Images + { mime = "image/*", fg = "#7fbbb3" }, + # Media + { mime = "{audio,video}/*", fg = "#d699b6" }, + # Archives + { mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", fg = "#e67e80" }, + # Documents + { mime = "application/{pdf,doc,rtf}", fg = "#7fbbb3" }, + # Virtual file system + { mime = "vfs/{absent,stale}", fg = "#4f585e" }, + # Fallback + { url = "*", fg = "#83c092" }, + { url = "*/", fg = "#a7c080" }, +] + +# : }}} diff --git a/.config/yazi/flavors/everforest-medium.yazi/preview.png b/.config/yazi/flavors/everforest-medium.yazi/preview.png new file mode 100644 index 0000000..005a9c3 Binary files /dev/null and b/.config/yazi/flavors/everforest-medium.yazi/preview.png differ diff --git a/.config/yazi/flavors/everforest-medium.yazi/tmtheme.xml b/.config/yazi/flavors/everforest-medium.yazi/tmtheme.xml new file mode 100644 index 0000000..e063c97 --- /dev/null +++ b/.config/yazi/flavors/everforest-medium.yazi/tmtheme.xml @@ -0,0 +1,3186 @@ + + + + + name + Everforest Dark + settings + + + settings + + accent + #a7c080 + background + #2d353b + caret + #d3c6aa + foreground + #d3c6aa + lineHighlight + #3d484d90 + selection + #475258c0 + activeGuide + #9aa79d20 + findHighlight + #899c4040 + misspelling + #da6362 + + + + name + GitGutter deleted + scope + markup.deleted.git_gutter + settings + + foreground + #e67e80a0 + + + + name + GitGutter inserted + scope + markup.inserted.git_gutter + settings + + foreground + #a7c080a0 + + + + name + GitGutter changed + scope + markup.changed.git_gutter + settings + + foreground + #7fbbb3a0 + + + + name + GitGutter untracked + scope + markup.untracked.git_gutter + settings + + foreground + #dbbc7fa0 + + + + name + GitGutter ignored + scope + markup.ignored.git_gutter + settings + + foreground + #4f585e + + + + name + GitGutter comment + scope + comment.line.annotation.git_gutter + settings + + foreground + + name + Comment + scope + comment, string.comment, punctuation.definition.comment + settings + + foreground + #859289 + fontStyle + italic + + + + + + name + Keyword + scope + keyword, storage.type.function, storage.type.class, storage.type.enum, storage.type.interface, storage.type.property, keyword.operator.new, keyword.operator.expression, keyword.operator.new, keyword.operator.delete, storage.type.extends + settings + + foreground + #e67e80 + + + + name + Debug + scope + keyword.other.debugger + settings + + foreground + #e67e80 + + + + name + Storage + scope + storage, modifier, keyword.var, entity.name.tag, keyword.control.case, keyword.control.switch + settings + + foreground + #e69875 + + + + name + Operator + scope + keyword.operator + settings + + foreground + #e69875 + + + + name + String + scope + string, punctuation.definition.string.end, punctuation.definition.string.begin, punctuation.definition.string.template.begin, punctuation.definition.string.template.end + settings + + foreground + #dbbc7f + + + + name + Attribute + scope + entity.other.attribute-name + settings + + foreground + #dbbc7f + + + + name + String Escape + scope + constant.character.escape, punctuation.quasi.element, punctuation.definition.template-expression, punctuation.section.embedded, storage.type.format, constant.other.placeholder, constant.other.placeholder, variable.interpolation + settings + + foreground + #a7c080 + + + + name + Function + scope + entity.name.function, support.function, meta.function, meta.function-call, meta.definition.method + settings + + foreground + #a7c080 + + + + name + Preproc + scope + keyword.control.at-rule, keyword.control.import, keyword.control.export, storage.type.namespace, punctuation.decorator, keyword.control.directive, keyword.preprocessor, punctuation.definition.preprocessor, punctuation.definition.directive, keyword.other.import, keyword.other.package, entity.name.type.namespace, entity.name.scope-resolution, keyword.other.using, keyword.package, keyword.import, keyword.map + settings + + foreground + #83c092 + + + + name + Annotation + scope + storage.type.annotation + settings + + foreground + #83c092 + + + + name + Label + scope + entity.name.label, constant.other.label + settings + + foreground + #83c092 + + + + name + Modules + scope + support.module, support.node, support.other.module, support.type.object.module, entity.name.type.module, entity.name.type.class.module, keyword.control.module + settings + + foreground + #83c092 + + + + name + Type + scope + storage.type, support.type, entity.name.type, keyword.type + settings + + foreground + #7fbbb3 + + + + name + Class + scope + entity.name.type.class, support.class, entity.name.class, entity.other.inherited-class, storage.class + settings + + foreground + #7fbbb3 + + + + name + Number + scope + constant.numeric + settings + + foreground + #d699b6 + + + + name + Boolean + scope + constant.language.boolean + settings + + foreground + #d699b6 + + + + name + Macro + scope + entity.name.function.preprocessor + settings + + foreground + #d699b6 + + + + name + Special identifier + scope + variable.language.this, variable.language.self, variable.language.super, keyword.other.this, variable.language.special, constant.language.null, constant.language.undefined, constant.language.nan + settings + + foreground + #d699b6 + + + + name + Constant + scope + constant.language, support.constant + settings + + foreground + #d699b6 + + + + name + Identifier + scope + variable, support.variable, meta.definition.variable + settings + + foreground + #d3c6aa + + + + name + Property + scope + variable.object.property, support.variable.property, variable.other.property, variable.other.object.property, variable.other.enummember, variable.other.member, meta.object-literal.key + settings + + foreground + #d3c6aa + + + + name + Delimiter + scope + punctuation, meta.brace, meta.delimiter, meta.bracket + settings + + foreground + #d3c6aa + + + + name + Markdown heading1 + scope + heading.1.markdown, markup.heading.setext.1.markdown + settings + + foreground + #e67e80 + fontStyle + bold + + + + name + Markdown heading2 + scope + heading.2.markdown, markup.heading.setext.2.markdown + settings + + foreground + #e69875 + fontStyle + bold + + + + name + Markdown heading3 + scope + heading.3.markdown + settings + + foreground + #dbbc7f + fontStyle + bold + + + + name + Markdown heading4 + scope + heading.4.markdown + settings + + foreground + #a7c080 + fontStyle + bold + + + + name + Markdown heading5 + scope + heading.5.markdown + settings + + foreground + #7fbbb3 + fontStyle + bold + + + + name + Markdown heading6 + scope + heading.6.markdown + settings + + foreground + #d699b6 + fontStyle + bold + + + + name + Markdown heading delimiter + scope + punctuation.definition.heading.markdown + settings + + foreground + #859289 + fontStyle + regular + + + + name + Markdown link + scope + string.other.link.title.markdown, constant.other.reference.link.markdown, string.other.link.description.markdown + settings + + foreground + #d699b6 + fontStyle + regular + + + + name + Markdown link text + scope + markup.underline.link.image.markdown, markup.underline.link.markdown + settings + + foreground + #a7c080 + fontStyle + underline + + + + name + Markdown delimiter + scope + punctuation.definition.string.begin.markdown, punctuation.definition.string.end.markdown, punctuation.definition.italic.markdown, punctuation.definition.quote.begin.markdown, punctuation.definition.metadata.markdown, punctuation.separator.key-value.markdown, punctuation.definition.constant.markdown + settings + + foreground + #859289 + + + + name + Markdown bold delimiter + scope + punctuation.definition.bold.markdown + settings + + foreground + #859289 + fontStyle + regular + + + + name + Markdown separator delimiter + scope + meta.separator.markdown, punctuation.definition.constant.begin.markdown, punctuation.definition.constant.end.markdown + settings + + foreground + #859289 + fontStyle + bold + + + + name + Markdown italic + scope + markup.italic + settings + + fontStyle + italic + + + + name + Markdown bold + scope + markup.bold + settings + + fontStyle + bold + + + + name + Markdown bold italic + scope + markup.bold markup.italic, markup.italic markup.bold + settings + + fontStyle + italic bold + + + + name + Markdown code delimiter + scope + punctuation.definition.markdown, punctuation.definition.raw.markdown + settings + + foreground + #dbbc7f + + + + name + Markdown code type + scope + fenced_code.block.language + settings + + foreground + #dbbc7f + + + + name + Markdown code block + scope + markup.fenced_code.block.markdown, markup.inline.raw.string.markdown + settings + + foreground + #a7c080 + + + + name + Markdown list mark + scope + punctuation.definition.list.begin.markdown + settings + + foreground + #e67e80 + + + + name + reStructuredText heading + scope + punctuation.definition.heading.restructuredtext + settings + + foreground + #e69875 + fontStyle + bold + + + + name + reStructuredText delimiter + scope + punctuation.definition.field.restructuredtext, punctuation.separator.key-value.restructuredtext, punctuation.definition.directive.restructuredtext, punctuation.definition.constant.restructuredtext, punctuation.definition.italic.restructuredtext, punctuation.definition.table.restructuredtext + settings + + foreground + #859289 + + + + name + reStructuredText delimiter bold + scope + punctuation.definition.bold.restructuredtext + settings + + foreground + #859289 + fontStyle + regular + + + + name + reStructuredText aqua + scope + entity.name.tag.restructuredtext, punctuation.definition.link.restructuredtext, punctuation.definition.raw.restructuredtext, punctuation.section.raw.restructuredtext + settings + + foreground + #83c092 + + + + name + reStructuredText purple + scope + constant.other.footnote.link.restructuredtext + settings + + foreground + #d699b6 + + + + name + reStructuredText red + scope + support.directive.restructuredtext + settings + + foreground + #e67e80 + + + + name + reStructuredText green + scope + entity.name.directive.restructuredtext, markup.raw.restructuredtext, markup.raw.inner.restructuredtext, string.other.link.title.restructuredtext + settings + + foreground + #a7c080 + + + + name + LaTex delimiter + scope + punctuation.definition.function.latex, punctuation.definition.function.tex, punctuation.definition.keyword.latex, constant.character.newline.tex, punctuation.definition.keyword.tex + settings + + foreground + #859289 + + + + name + LaTex red + scope + support.function.be.latex + settings + + foreground + #e67e80 + + + + name + LaTex orange + scope + support.function.section.latex, keyword.control.table.cell.latex, keyword.control.table.newline.latex + settings + + foreground + #e69875 + + + + name + LaTex yellow + scope + support.class.latex, variable.parameter.latex, variable.parameter.function.latex, variable.parameter.definition.label.latex, constant.other.reference.label.latex + settings + + foreground + #dbbc7f + + + + name + LaTex purple + scope + keyword.control.preamble.latex + settings + + foreground + #d699b6 + + + + name + Html grey + scope + punctuation.separator.namespace.xml + settings + + foreground + #859289 + + + + name + Html orange + scope + entity.name.tag.html, entity.name.tag.xml, entity.name.tag.localname.xml + settings + + foreground + #e69875 + + + + name + Html yellow + scope + entity.other.attribute-name.html, entity.other.attribute-name.xml, entity.other.attribute-name.localname.xml + settings + + foreground + #dbbc7f + + + + name + Html green + scope + string.quoted.double.html, string.quoted.single.html, punctuation.definition.string.begin.html, punctuation.definition.string.end.html, punctuation.separator.key-value.html, punctuation.definition.string.begin.xml, punctuation.definition.string.end.xml, string.quoted.double.xml, string.quoted.single.xml, punctuation.definition.tag.begin.html, punctuation.definition.tag.end.html, punctuation.definition.tag.xml, meta.tag.xml, meta.tag.preprocessor.xml, meta.tag.other.html, meta.tag.block.any.html, meta.tag.inline.any.html + settings + + foreground + #a7c080 + + + + name + Html purple + scope + variable.language.documentroot.xml, meta.tag.sgml.doctype.xml + settings + + foreground + #d699b6 + + + + name + Proto yellow + scope + storage.type.proto + settings + + foreground + #dbbc7f + + + + name + Proto green + scope + string.quoted.double.proto.syntax, string.quoted.single.proto.syntax, string.quoted.double.proto, string.quoted.single.proto + settings + + foreground + #a7c080 + + + + name + Proto aqua + scope + entity.name.class.proto, entity.name.class.message.proto + settings + + foreground + #83c092 + + + + name + CSS grey + scope + punctuation.definition.entity.css, punctuation.separator.key-value.css, punctuation.terminator.rule.css, punctuation.separator.list.comma.css + settings + + foreground + #859289 + + + + name + CSS red + scope + entity.other.attribute-name.class.css + settings + + foreground + #e67e80 + + + + name + CSS orange + scope + keyword.other.unit + settings + + foreground + #e69875 + + + + name + CSS yellow + scope + entity.other.attribute-name.pseudo-class.css, entity.other.attribute-name.pseudo-element.css + settings + + foreground + #dbbc7f + + + + name + CSS green + scope + string.quoted.single.css, string.quoted.double.css, support.constant.property-value.css, meta.property-value.css, punctuation.definition.string.begin.css, punctuation.definition.string.end.css, constant.numeric.css, support.constant.font-name.css, variable.parameter.keyframe-list.css + settings + + foreground + #a7c080 + + + + name + CSS aqua + scope + support.type.property-name.css + settings + + foreground + #83c092 + + + + name + CSS blue + scope + support.type.vendored.property-name.css + settings + + foreground + #7fbbb3 + + + + name + CSS purple + scope + entity.name.tag.css, entity.other.keyframe-offset.css, punctuation.definition.keyword.css, keyword.control.at-rule.keyframes.css, meta.selector.css + settings + + foreground + #d699b6 + + + + name + SASS grey + scope + punctuation.definition.entity.scss, punctuation.separator.key-value.scss, punctuation.terminator.rule.scss, punctuation.separator.list.comma.scss + settings + + foreground + #859289 + + + + name + SASS orange + scope + keyword.control.at-rule.keyframes.scss + settings + + foreground + #e69875 + + + + name + SASS yellow + scope + punctuation.definition.interpolation.begin.bracket.curly.scss, punctuation.definition.interpolation.end.bracket.curly.scss + settings + + foreground + #dbbc7f + + + + name + SASS green + scope + punctuation.definition.string.begin.scss, punctuation.definition.string.end.scss, string.quoted.double.scss, string.quoted.single.scss, constant.character.css.sass, meta.property-value.scss + settings + + foreground + #a7c080 + + + + name + SASS purple + scope + keyword.control.at-rule.include.scss, keyword.control.at-rule.use.scss, keyword.control.at-rule.mixin.scss, keyword.control.at-rule.extend.scss, keyword.control.at-rule.import.scss + settings + + foreground + #d699b6 + + + + name + Stylus white + scope + meta.function.stylus + settings + + foreground + #d3c6aa + + + + name + Stylus yellow + scope + entity.name.function.stylus + settings + + foreground + #dbbc7f + + + + name + JavaScript white + scope + string.unquoted.js + settings + + foreground + #d3c6aa + + + + name + JavaScript grey + scope + punctuation.accessor.js, punctuation.separator.key-value.js, punctuation.separator.label.js, keyword.operator.accessor.js + settings + + foreground + #859289 + + + + name + JavaScript red + scope + punctuation.definition.block.tag.jsdoc + settings + + foreground + #e67e80 + + + + name + JavaScript orange + scope + storage.type.js, storage.type.function.arrow.js + settings + + foreground + #e69875 + + + + name + JSX white + scope + JSXNested + settings + + foreground + #d3c6aa + + + + name + JSX green + scope + punctuation.definition.tag.jsx, entity.other.attribute-name.jsx, punctuation.definition.tag.begin.js.jsx, punctuation.definition.tag.end.js.jsx, entity.other.attribute-name.js.jsx + settings + + foreground + #a7c080 + + + + name + TypeScript white + scope + entity.name.type.module.ts + settings + + foreground + #d3c6aa + + + + name + TypeScript grey + scope + keyword.operator.type.annotation.ts, punctuation.accessor.ts, punctuation.separator.key-value.ts + settings + + foreground + #859289 + + + + name + TypeScript green + scope + punctuation.definition.tag.directive.ts, entity.other.attribute-name.directive.ts + settings + + foreground + #a7c080 + + + + name + TypeScript aqua + scope + entity.name.type.ts, entity.name.type.interface.ts, entity.other.inherited-class.ts, entity.name.type.alias.ts, entity.name.type.class.ts, entity.name.type.enum.ts + settings + + foreground + #83c092 + + + + name + TypeScript orange + scope + storage.type.ts, storage.type.function.arrow.ts, storage.type.type.ts + settings + + foreground + #e69875 + + + + name + TypeScript blue + scope + entity.name.type.module.ts + settings + + foreground + #7fbbb3 + + + + name + TypeScript purple + scope + keyword.control.import.ts, keyword.control.export.ts, storage.type.namespace.ts + settings + + foreground + #d699b6 + + + + name + TSX white + scope + entity.name.type.module.tsx + settings + + foreground + #d3c6aa + + + + name + TSX grey + scope + keyword.operator.type.annotation.tsx, punctuation.accessor.tsx, punctuation.separator.key-value.tsx + settings + + foreground + #859289 + + + + name + TSX green + scope + punctuation.definition.tag.directive.tsx, entity.other.attribute-name.directive.tsx, punctuation.definition.tag.begin.tsx, punctuation.definition.tag.end.tsx, entity.other.attribute-name.tsx + settings + + foreground + #a7c080 + + + + name + TSX aqua + scope + entity.name.type.tsx, entity.name.type.interface.tsx, entity.other.inherited-class.tsx, entity.name.type.alias.tsx, entity.name.type.class.tsx, entity.name.type.enum.tsx + settings + + foreground + #83c092 + + + + name + TSX blue + scope + entity.name.type.module.tsx + settings + + foreground + #7fbbb3 + + + + name + TSX purple + scope + keyword.control.import.tsx, keyword.control.export.tsx, storage.type.namespace.tsx + settings + + foreground + #d699b6 + + + + name + TSX orange + scope + storage.type.tsx, storage.type.function.arrow.tsx, storage.type.type.tsx, support.class.component.tsx + settings + + foreground + #e69875 + + + + name + CoffeeScript orange + scope + storage.type.function.coffee + settings + + foreground + #e69875 + + + + name + PureScript white + scope + meta.type-signature.purescript + settings + + foreground + #d3c6aa + + + + name + PureScript orange + scope + keyword.other.double-colon.purescript, keyword.other.arrow.purescript, keyword.other.big-arrow.purescript + settings + + foreground + #e69875 + + + + name + PureScript yellow + scope + entity.name.function.purescript + settings + + foreground + #dbbc7f + + + + name + PureScript green + scope + string.quoted.single.purescript, string.quoted.double.purescript, punctuation.definition.string.begin.purescript, punctuation.definition.string.end.purescript, string.quoted.triple.purescript, entity.name.type.purescript + settings + + foreground + #a7c080 + + + + name + PureScript purple + scope + support.other.module.purescript + settings + + foreground + #d699b6 + + + + name + Dart grey + scope + punctuation.dot.dart + settings + + foreground + #859289 + + + + name + Dart orange + scope + storage.type.primitive.dart + settings + + foreground + #e69875 + + + + name + Dart yellow + scope + support.class.dart + settings + + foreground + #dbbc7f + + + + name + Dart green + scope + entity.name.function.dart, string.interpolated.single.dart, string.interpolated.double.dart + settings + + foreground + #a7c080 + + + + name + Dart blue + scope + variable.language.dart + settings + + foreground + #7fbbb3 + + + + name + Dart purple + scope + keyword.other.import.dart, storage.type.annotation.dart + settings + + foreground + #d699b6 + + + + name + Pug red + scope + entity.other.attribute-name.class.pug + settings + + foreground + #e67e80 + + + + name + Pug orange + scope + storage.type.function.pug + settings + + foreground + #e69875 + + + + name + Pug aqua + scope + entity.other.attribute-name.tag.pug + settings + + foreground + #83c092 + + + + name + Pug purple + scope + entity.name.tag.pug, storage.type.import.include.pug + settings + + foreground + #d699b6 + + + + name + C white + scope + meta.function-call.c, storage.modifier.array.bracket.square.c, meta.function.definition.parameters.c + settings + + foreground + #d3c6aa + + + + name + C grey + scope + punctuation.separator.dot-access.c, constant.character.escape.line-continuation.c + settings + + foreground + #859289 + + + + name + C red + scope + keyword.control.directive.include.c, punctuation.definition.directive.c, keyword.control.directive.pragma.c, keyword.control.directive.line.c, keyword.control.directive.define.c, keyword.control.directive.conditional.c, keyword.control.directive.diagnostic.error.c, keyword.control.directive.undef.c, keyword.control.directive.conditional.ifdef.c, keyword.control.directive.endif.c, keyword.control.directive.conditional.ifndef.c, keyword.control.directive.conditional.if.c, keyword.control.directive.else.c + settings + + foreground + #e67e80 + + + + name + C orange + scope + punctuation.separator.pointer-access.c + settings + + foreground + #e69875 + + + + name + C aqua + scope + variable.other.member.c + settings + + foreground + #83c092 + + + + name + C++ white + scope + meta.function-call.cpp, storage.modifier.array.bracket.square.cpp, meta.function.definition.parameters.cpp, meta.body.function.definition.cpp + settings + + foreground + #d3c6aa + + + + name + C++ grey + scope + punctuation.separator.dot-access.cpp, constant.character.escape.line-continuation.cpp + settings + + foreground + #859289 + + + + name + C++ red + scope + keyword.control.directive.include.cpp, punctuation.definition.directive.cpp, keyword.control.directive.pragma.cpp, keyword.control.directive.line.cpp, keyword.control.directive.define.cpp, keyword.control.directive.conditional.cpp, keyword.control.directive.diagnostic.error.cpp, keyword.control.directive.undef.cpp, keyword.control.directive.conditional.ifdef.cpp, keyword.control.directive.endif.cpp, keyword.control.directive.conditional.ifndef.cpp, keyword.control.directive.conditional.if.cpp, keyword.control.directive.else.cpp, storage.type.namespace.definition.cpp, keyword.other.using.directive.cpp, storage.type.struct.cpp + settings + + foreground + #e67e80 + + + + name + C++ orange + scope + punctuation.separator.pointer-access.cpp, punctuation.section.angle-brackets.begin.template.call.cpp, punctuation.section.angle-brackets.end.template.call.cpp + settings + + foreground + #e69875 + + + + name + C++ aqua + scope + variable.other.member.cpp + settings + + foreground + #83c092 + + + + name + C# red + scope + keyword.other.using.cs + settings + + foreground + #e67e80 + + + + name + C# yellow + scope + keyword.type.cs, constant.character.escape.cs, punctuation.definition.interpolation.begin.cs, punctuation.definition.interpolation.end.cs + settings + + foreground + #dbbc7f + + + + name + C# green + scope + string.quoted.double.cs, string.quoted.single.cs, punctuation.definition.string.begin.cs, punctuation.definition.string.end.cs + settings + + foreground + #a7c080 + + + + name + C# aqua + scope + variable.other.object.property.cs + settings + + foreground + #83c092 + + + + name + C# purple + scope + entity.name.type.namespace.cs + settings + + foreground + #d699b6 + + + + name + F# white + scope + keyword.symbol.fsharp, constant.language.unit.fsharp + settings + + foreground + #d3c6aa + + + + name + F# yellow + scope + keyword.format.specifier.fsharp, entity.name.type.fsharp + settings + + foreground + #dbbc7f + + + + name + F# green + scope + string.quoted.double.fsharp, string.quoted.single.fsharp, punctuation.definition.string.begin.fsharp, punctuation.definition.string.end.fsharp + settings + + foreground + #a7c080 + + + + name + F# blue + scope + entity.name.section.fsharp + settings + + foreground + #7fbbb3 + + + + name + F# purple + scope + support.function.attribute.fsharp + settings + + foreground + #d699b6 + + + + name + Java grey + scope + punctuation.separator.java, punctuation.separator.period.java + settings + + foreground + #859289 + + + + name + Java red + scope + keyword.other.import.java, keyword.other.package.java + settings + + foreground + #e67e80 + + + + name + Java orange + scope + storage.type.function.arrow.java, keyword.control.ternary.java + settings + + foreground + #e69875 + + + + name + Java aqua + scope + variable.other.property.java + settings + + foreground + #83c092 + + + + name + Java purple + scope + variable.language.wildcard.java, storage.modifier.import.java, storage.type.annotation.java, punctuation.definition.annotation.java, storage.modifier.package.java, entity.name.type.module.java + settings + + foreground + #d699b6 + + + + name + Kotlin red + scope + keyword.other.import.kotlin + settings + + foreground + #e67e80 + + + + name + Kotlin orange + scope + storage.type.kotlin + settings + + foreground + #e69875 + + + + name + Kotlin aqua + scope + constant.language.kotlin + settings + + foreground + #83c092 + + + + name + Kotlin purple + scope + entity.name.package.kotlin, storage.type.annotation.kotlin + settings + + foreground + #d699b6 + + + + name + Scala purple + scope + entity.name.package.scala + settings + + foreground + #d699b6 + + + + name + Scala blue + scope + constant.language.scala + settings + + foreground + #7fbbb3 + + + + name + Scala aqua + scope + entity.name.import.scala + settings + + foreground + #83c092 + + + + name + Scala green + scope + string.quoted.double.scala, string.quoted.single.scala, punctuation.definition.string.begin.scala, punctuation.definition.string.end.scala, string.quoted.double.interpolated.scala, string.quoted.single.interpolated.scala, string.quoted.triple.scala + settings + + foreground + #a7c080 + + + + name + Scala yellow + scope + entity.name.class, entity.other.inherited-class.scala + settings + + foreground + #dbbc7f + + + + name + Scala orange + scope + keyword.declaration.stable.scala, keyword.other.arrow.scala + settings + + foreground + #e69875 + + + + name + Scala red + scope + keyword.other.import.scala + settings + + foreground + #e67e80 + + + + name + Groovy white + scope + keyword.operator.navigation.groovy, meta.method.body.java, meta.definition.method.groovy, meta.definition.method.signature.java + settings + + foreground + #d3c6aa + + + + name + Scala grey + scope + punctuation.separator.groovy + settings + + foreground + #859289 + + + + name + Scala red + scope + keyword.other.import.groovy, keyword.other.package.groovy, keyword.other.import.static.groovy + settings + + foreground + #e67e80 + + + + name + Groovy orange + scope + storage.type.def.groovy + settings + + foreground + #e69875 + + + + name + Groovy green + scope + variable.other.interpolated.groovy, meta.method.groovy + settings + + foreground + #a7c080 + + + + name + Groovy aqua + scope + storage.modifier.import.groovy, storage.modifier.package.groovy + settings + + foreground + #83c092 + + + + name + Groovy purple + scope + storage.type.annotation.groovy + settings + + foreground + #d699b6 + + + + name + Go red + scope + keyword.type.go + settings + + foreground + #e67e80 + + + + name + Go aqua + scope + entity.name.package.go + settings + + foreground + #83c092 + + + + name + Go purple + scope + keyword.import.go, keyword.package.go + settings + + foreground + #d699b6 + + + + name + Rust white + scope + entity.name.type.mod.rust + settings + + foreground + #d3c6aa + + + + name + Rust grey + scope + keyword.operator.path.rust, keyword.operator.member-access.rust + settings + + foreground + #859289 + + + + name + Rust orange + scope + storage.type.rust + settings + + foreground + #e69875 + + + + name + Rust aqua + scope + support.constant.core.rust + settings + + foreground + #83c092 + + + + name + Rust purple + scope + meta.attribute.rust, variable.language.rust, storage.type.module.rust + settings + + foreground + #d699b6 + + + + name + Swift white + scope + meta.function-call.swift, support.function.any-method.swift + settings + + foreground + #d3c6aa + + + + name + Swift aqua + scope + support.variable.swift + settings + + foreground + #83c092 + + + + name + PHP white + scope + keyword.operator.class.php + settings + + foreground + #d3c6aa + + + + name + PHP orange + scope + storage.type.trait.php + settings + + foreground + #e69875 + + + + name + PHP aqua + scope + constant.language.php, support.other.namespace.php + settings + + foreground + #83c092 + + + + name + PHP blue + scope + storage.type.modifier.access.control.public.cpp, storage.type.modifier.access.control.private.cpp + settings + + foreground + #7fbbb3 + + + + name + PHP purple + scope + keyword.control.import.include.php, storage.type.php + settings + + foreground + #d699b6 + + + + name + Python white + scope + meta.function-call.arguments.python + settings + + foreground + #d3c6aa + + + + name + Python grey + scope + punctuation.definition.decorator.python, punctuation.separator.period.python + settings + + foreground + #859289 + + + + name + Python aqua + scope + constant.language.python + settings + + foreground + #83c092 + + + + name + Python purple + scope + keyword.control.import.python, keyword.control.import.from.python + settings + + foreground + #d699b6 + + + + name + Lua aqua + scope + constant.language.lua + settings + + foreground + #83c092 + + + + name + Lua blue + scope + entity.name.class.lua + settings + + foreground + #7fbbb3 + + + + name + Ruby white + scope + meta.function.method.with-arguments.ruby + settings + + foreground + #d3c6aa + + + + name + Ruby grey + scope + punctuation.separator.method.ruby + settings + + foreground + #859289 + + + + name + Ruby orange + scope + keyword.control.pseudo-method.ruby, storage.type.variable.ruby + settings + + foreground + #e69875 + + + + name + Ruby green + scope + keyword.other.special-method.ruby + settings + + foreground + #a7c080 + + + + name + Ruby purple + scope + keyword.control.module.ruby, punctuation.definition.constant.ruby + settings + + foreground + #d699b6 + + + + name + Ruby yellow + scope + string.regexp.character-class.ruby,string.regexp.interpolated.ruby,punctuation.definition.character-class.ruby,string.regexp.group.ruby, punctuation.section.regexp.ruby, punctuation.definition.group.ruby + settings + + foreground + #dbbc7f + + + + name + Ruby blue + scope + variable.other.constant.ruby + settings + + foreground + #7fbbb3 + + + + name + Haskell orange + scope + keyword.other.arrow.haskell, keyword.other.big-arrow.haskell, keyword.other.double-colon.haskell + settings + + foreground + #e69875 + + + + name + Haskell yellow + scope + storage.type.haskell + settings + + foreground + #dbbc7f + + + + name + Haskell green + scope + constant.other.haskell, string.quoted.double.haskell, string.quoted.single.haskell, punctuation.definition.string.begin.haskell, punctuation.definition.string.end.haskell + settings + + foreground + #a7c080 + + + + name + Haskell blue + scope + entity.name.function.haskell + settings + + foreground + #7fbbb3 + + + + name + Haskell aqua + scope + entity.name.namespace, meta.preprocessor.haskell + settings + + foreground + #83c092 + + + + name + Julia red + scope + keyword.control.import.julia, keyword.control.export.julia + settings + + foreground + #e67e80 + + + + name + Julia orange + scope + keyword.storage.modifier.julia + settings + + foreground + #e69875 + + + + name + Julia aqua + scope + constant.language.julia + settings + + foreground + #83c092 + + + + name + Julia purple + scope + support.function.macro.julia + settings + + foreground + #d699b6 + + + + name + Elm white + scope + keyword.other.period.elm + settings + + foreground + #d3c6aa + + + + name + Elm yellow + scope + storage.type.elm + settings + + foreground + #dbbc7f + + + + name + R orange + scope + keyword.other.r + settings + + foreground + #e69875 + + + + name + R green + scope + entity.name.function.r, variable.function.r + settings + + foreground + #a7c080 + + + + name + R aqua + scope + constant.language.r + settings + + foreground + #83c092 + + + + name + R purple + scope + entity.namespace.r + settings + + foreground + #d699b6 + + + + name + Erlang grey + scope + punctuation.separator.module-function.erlang, punctuation.section.directive.begin.erlang + settings + + foreground + #859289 + + + + name + Erlang red + scope + keyword.control.directive.erlang, keyword.control.directive.define.erlang + settings + + foreground + #e67e80 + + + + name + Erlang yellow + scope + entity.name.type.class.module.erlang + settings + + foreground + #dbbc7f + + + + name + Erlang green + scope + string.quoted.double.erlang, string.quoted.single.erlang, punctuation.definition.string.begin.erlang, punctuation.definition.string.end.erlang + settings + + foreground + #a7c080 + + + + name + Erlang purple + scope + keyword.control.directive.export.erlang, keyword.control.directive.module.erlang, keyword.control.directive.import.erlang, keyword.control.directive.behaviour.erlang + settings + + foreground + #d699b6 + + + + name + Elixir aqua + scope + variable.other.readwrite.module.elixir, punctuation.definition.variable.elixir + settings + + foreground + #83c092 + + + + name + Elixir blue + scope + constant.language.elixir + settings + + foreground + #7fbbb3 + + + + name + Elixir purple + scope + keyword.control.module.elixir + settings + + foreground + #d699b6 + + + + name + OCaml white + scope + entity.name.type.value-signature.ocaml + settings + + foreground + #d3c6aa + + + + name + OCaml orange + scope + keyword.other.ocaml + settings + + foreground + #e69875 + + + + name + OCaml aqua + scope + constant.language.variant.ocaml + settings + + foreground + #83c092 + + + + name + Perl red + scope + storage.type.sub.perl, storage.type.declare.routine.perl + settings + + foreground + #e67e80 + + + + name + Lisp white + scope + meta.function.lisp + settings + + foreground + #d3c6aa + + + + name + Lisp red + scope + storage.type.function-type.lisp + settings + + foreground + #e67e80 + + + + name + Lisp green + scope + keyword.constant.lisp + settings + + foreground + #a7c080 + + + + name + Lisp aqua + scope + entity.name.function.lisp + settings + + foreground + #83c092 + + + + name + Clojure green + scope + constant.keyword.clojure, support.variable.clojure, meta.definition.variable.clojure + settings + + foreground + #a7c080 + + + + name + Clojure purple + scope + entity.global.clojure + settings + + foreground + #d699b6 + + + + name + Clojure blue + scope + entity.name.function.clojure + settings + + foreground + #7fbbb3 + + + + name + Shell white + scope + meta.scope.if-block.shell, meta.scope.group.shell + settings + + foreground + #d3c6aa + + + + name + Shell yellow + scope + support.function.builtin.shell, entity.name.function.shell + settings + + foreground + #dbbc7f + + + + name + Shell green + scope + string.quoted.double.shell, string.quoted.single.shell, punctuation.definition.string.begin.shell, punctuation.definition.string.end.shell, string.unquoted.heredoc.shell + settings + + foreground + #a7c080 + + + + name + Shell purple + scope + keyword.control.heredoc-token.shell, variable.other.normal.shell, punctuation.definition.variable.shell, variable.other.special.shell, variable.other.positional.shell, variable.other.bracket.shell + settings + + foreground + #d699b6 + + + + name + Fish red + scope + support.function.builtin.fish + settings + + foreground + #e67e80 + + + + name + Fish orange + scope + support.function.unix.fish + settings + + foreground + #e69875 + + + + name + Fish blue + scope + variable.other.normal.fish, punctuation.definition.variable.fish, variable.other.fixed.fish, variable.other.special.fish + settings + + foreground + #7fbbb3 + + + + name + Fish green + scope + string.quoted.double.fish, punctuation.definition.string.end.fish, punctuation.definition.string.begin.fish, string.quoted.single.fish + settings + + foreground + #a7c080 + + + + name + Fish purple + scope + constant.character.escape.single.fish + settings + + foreground + #d699b6 + + + + name + PowerShell grey + scope + punctuation.definition.variable.powershell + settings + + foreground + #859289 + + + + name + PowerShell yellow + scope + entity.name.function.powershell, support.function.attribute.powershell, support.function.powershell + settings + + foreground + #dbbc7f + + + + name + PowerShell green + scope + string.quoted.single.powershell, string.quoted.double.powershell, punctuation.definition.string.begin.powershell, punctuation.definition.string.end.powershell, string.quoted.double.heredoc.powershell + settings + + foreground + #a7c080 + + + + name + PowerShell aqua + scope + variable.other.member.powershell + settings + + foreground + #83c092 + + + + name + GraphQL white + scope + string.unquoted.alias.graphql + settings + + foreground + #d3c6aa + + + + name + GraphQL red + scope + keyword.type.graphql + settings + + foreground + #e67e80 + + + + name + GraphQL purple + scope + entity.name.fragment.graphql + settings + + foreground + #d699b6 + + + + name + Makefile orange + scope + entity.name.function.target.makefile + settings + + foreground + #e69875 + + + + name + Makefile yellow + scope + variable.other.makefile + settings + + foreground + #dbbc7f + + + + name + Makefile green + scope + meta.scope.prerequisites.makefile + settings + + foreground + #a7c080 + + + + name + CMake green + scope + string.source.cmake + settings + + foreground + #a7c080 + + + + name + CMake aqua + scope + entity.source.cmake + settings + + foreground + #83c092 + + + + name + CMake purple + scope + storage.source.cmake + settings + + foreground + #d699b6 + + + + name + VimL grey + scope + punctuation.definition.map.viml + settings + + foreground + #859289 + + + + name + VimL orange + scope + storage.type.map.viml + settings + + foreground + #e69875 + + + + name + VimL green + scope + constant.character.map.viml, constant.character.map.key.viml + settings + + foreground + #a7c080 + + + + name + VimL blue + scope + constant.character.map.special.viml + settings + + foreground + #7fbbb3 + + + + name + Tmux green + scope + constant.language.tmux, constant.numeric.tmux + settings + + foreground + #a7c080 + + + + name + Dockerfile orange + scope + entity.name.function.package-manager.dockerfile + settings + + foreground + #e69875 + + + + name + Dockerfile yellow + scope + keyword.operator.flag.dockerfile + settings + + foreground + #dbbc7f + + + + name + Dockerfile green + scope + string.quoted.double.dockerfile, string.quoted.single.dockerfile + settings + + foreground + #a7c080 + + + + name + Dockerfile aqua + scope + constant.character.escape.dockerfile + settings + + foreground + #83c092 + + + + name + Dockerfile purple + scope + entity.name.type.base-image.dockerfile, entity.name.image.dockerfile + settings + + foreground + #d699b6 + + + + name + Diff grey + scope + punctuation.definition.separator.diff + settings + + foreground + #859289 + + + + name + Diff red + scope + markup.deleted.diff, punctuation.definition.deleted.diff + settings + + foreground + #e67e80 + + + + name + Diff orange + scope + meta.diff.range.context, punctuation.definition.range.diff + settings + + foreground + #e69875 + + + + name + Diff yellow + scope + meta.diff.header.from-file + settings + + foreground + #dbbc7f + + + + name + Diff green + scope + markup.inserted.diff, punctuation.definition.inserted.diff + settings + + foreground + #a7c080 + + + + name + Diff blue + scope + markup.changed.diff, punctuation.definition.changed.diff + settings + + foreground + #7fbbb3 + + + + name + Diff purple + scope + punctuation.definition.from-file.diff + settings + + foreground + #d699b6 + + + + name + Git red + scope + entity.name.section.group-title.ini, punctuation.definition.entity.ini + settings + + foreground + #e67e80 + + + + name + Git orange + scope + punctuation.separator.key-value.ini + settings + + foreground + #e69875 + + + + name + Git green + scope + string.quoted.double.ini, string.quoted.single.ini, punctuation.definition.string.begin.ini, punctuation.definition.string.end.ini + settings + + foreground + #a7c080 + + + + name + Git aqua + scope + keyword.other.definition.ini + settings + + foreground + #83c092 + + + + name + SQL yellow + scope + support.function.aggregate.sql + settings + + foreground + #dbbc7f + + + + name + SQL green + scope + string.quoted.single.sql, punctuation.definition.string.end.sql, punctuation.definition.string.begin.sql, string.quoted.double.sql + settings + + foreground + #a7c080 + + + + name + GraphQL yellow + scope + support.type.graphql + settings + + foreground + #dbbc7f + + + + name + GraphQL blue + scope + variable.parameter.graphql + settings + + foreground + #7fbbb3 + + + + name + GraphQL aqua + scope + constant.character.enum.graphql + settings + + foreground + #83c092 + + + + name + JSON grey + scope + punctuation.support.type.property-name.begin.json, punctuation.support.type.property-name.end.json, punctuation.separator.dictionary.key-value.json, punctuation.definition.string.begin.json, punctuation.definition.string.end.json, punctuation.separator.dictionary.pair.json, punctuation.separator.array.json + settings + + foreground + #859289 + + + + name + JSON orange + scope + support.type.property-name.json + settings + + foreground + #e69875 + + + + name + JSON green + scope + string.quoted.double.json + settings + + foreground + #a7c080 + + + + name + YAML grey + scope + punctuation.separator.key-value.mapping.yaml + settings + + foreground + #859289 + + + + name + YAML green + scope + string.unquoted.plain.out.yaml, string.quoted.single.yaml, string.quoted.double.yaml, punctuation.definition.string.begin.yaml, punctuation.definition.string.end.yaml, string.unquoted.plain.in.yaml, string.unquoted.block.yaml + settings + + foreground + #a7c080 + + + + name + YAML aqua + scope + punctuation.definition.anchor.yaml, punctuation.definition.block.sequence.item.yaml + settings + + foreground + #83c092 + + + + name + TOML orange + scope + keyword.key.toml + settings + + foreground + #e69875 + + + + name + TOML green + scope + string.quoted.single.basic.line.toml, string.quoted.single.literal.line.toml, punctuation.definition.keyValuePair.toml + settings + + foreground + #a7c080 + + + + name + TOML blue + scope + constant.other.boolean.toml + settings + + foreground + #7fbbb3 + + + + name + TOML purple + scope + entity.other.attribute-name.table.toml, punctuation.definition.table.toml, entity.other.attribute-name.table.array.toml, punctuation.definition.table.array.toml + settings + + foreground + #d699b6 + + + + name + Comment + scope + comment, string.comment, punctuation.definition.comment + settings + + foreground + #859289 + fontStyle + italic + + + + uuid + 3f688e48-bd62-4cd7-9981-9b491786d8c6 + colorSpaceName + sRGB + semanticClass + theme.dark.everforest-dark + author + + comment + + + diff --git a/.config/yazi/flavors/gruvbox-material.yazi/LICENSE b/.config/yazi/flavors/gruvbox-material.yazi/LICENSE new file mode 100644 index 0000000..89d0a34 --- /dev/null +++ b/.config/yazi/flavors/gruvbox-material.yazi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 - Matthew Dong + +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/.config/yazi/flavors/gruvbox-material.yazi/LICENSE-tmtheme b/.config/yazi/flavors/gruvbox-material.yazi/LICENSE-tmtheme new file mode 100644 index 0000000..11069ed --- /dev/null +++ b/.config/yazi/flavors/gruvbox-material.yazi/LICENSE-tmtheme @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/.config/yazi/flavors/gruvbox-material.yazi/README.md b/.config/yazi/flavors/gruvbox-material.yazi/README.md new file mode 100644 index 0000000..6bd4607 --- /dev/null +++ b/.config/yazi/flavors/gruvbox-material.yazi/README.md @@ -0,0 +1,42 @@ +
+ Yazi logo +
+ +

+ Gruvbox Material Flavor for Yazi +

+ +## 👀 Preview + +![gruvbox-material](./preview.png) + +## 🎹 Installation + +### Using package manager + +```bash +ya pkg add matt-dong-123/gruvbox-material +``` + +### Manual install + +```bash +# Linux/macOS +git clone https://github.com/matt-dong-123/gruvbox-material.yazi.git ~/.config/yazi/flavors/gruvbox-material.yazi + +# Windows +git clone https://github.com/matt-dong-123/gruvbox-material.yazi.git %AppData%\yazi\config\flavors\gruvbox-material.yazi +``` + +## ⚙ Usage + +Add the these lines to your `theme.toml` configuration file to use it: + +```toml +[flavor] +dark = "gruvbox-material" +``` + +## 📜 License + +Check the [LICENSE](LICENSE) file for more details. diff --git a/.config/yazi/flavors/gruvbox-material.yazi/flavor.toml b/.config/yazi/flavors/gruvbox-material.yazi/flavor.toml new file mode 100644 index 0000000..d01918c --- /dev/null +++ b/.config/yazi/flavors/gruvbox-material.yazi/flavor.toml @@ -0,0 +1,214 @@ +# vim:fileencoding=utf-8:foldmethod=marker + +# : Manager {{{ + +[mgr] +cwd = { fg = "#89b482" } + +# Find +find_keyword = { fg = "#d8a657", bold = true, italic = true, underline = true } +find_position = { fg = "#d3869b", bg = "reset", bold = true, italic = true } + +# Marker +marker_copied = { fg = "#a9b665", bg = "#a9b665" } +marker_cut = { fg = "#ea6962", bg = "#ea6962" } +marker_marked = { fg = "#89b482", bg = "#89b482" } +marker_selected = { fg = "#d8a657", bg = "#d8a657" } + +# Count +count_copied = { fg = "#282828", bg = "#a9b665" } +count_cut = { fg = "#282828", bg = "#ea6962" } +count_selected = { fg = "#282828", bg = "#d8a657" } + +# Border +border_symbol = "│" +border_style = { fg = "#928374" } + +# : }}} + + +# : Tabs {{{ + +[tabs] +active = { fg = "#282828", bg = "#7daea3", bold = true } +inactive = { fg = "#7daea3", bg = "#32302f" } + +# : }}} + + +# : Mode {{{ + +[mode] + +normal_main = { fg = "#282828", bg = "#7daea3", bold = true } +normal_alt = { fg = "#7daea3", bg = "#32302f" } + +# Select mode +select_main = { fg = "#282828", bg = "#89b482", bold = true } +select_alt = { fg = "#89b482", bg = "#32302f" } + +# Unset mode +unset_main = { fg = "#282828", bg = "#ea6962", bold = true } +unset_alt = { fg = "#ea6962", bg = "#32302f" } + +# : }}} + + +# : Status bar {{{ + +[status] +# Permissions +perm_sep = { fg = "#928374" } +perm_type = { fg = "#7daea3" } +perm_read = { fg = "#d8a657" } +perm_write = { fg = "#ea6962" } +perm_exec = { fg = "#a9b665" } + +# Progress +progress_label = { fg = "#ddc7a1", bold = true } +progress_normal = { fg = "#a9b665", bg = "#45403d" } +progress_error = { fg = "#d8a657", bg = "#ea6962" } + +# : }}} + + +# : Pick {{{ + +[pick] +border = { fg = "#7daea3" } +active = { fg = "#d3869b", bold = true } +inactive = {} + +# : }}} + + +# : Input {{{ + +[input] +border = { fg = "#7daea3" } +title = {} +value = {} +selected = { reversed = true } + +# : }}} + + +# : Completion {{{ + +[cmp] +border = { fg = "#7daea3" } + +# : }}} + + +# : Tasks {{{ + +[tasks] +border = { fg = "#7daea3" } +title = {} +hovered = { fg = "#d3869b", bold = true } + +# : }}} + + +# : Which {{{ + +[which] +mask = { bg = "#32302f" } +cand = { fg = "#89b482" } +rest = { fg = "#a89984" } +desc = { fg = "#d3869b" } +separator = " îȘœ " +separator_style = { fg = "#7c6f64" } + +# : }}} + + +# : Help {{{ + +[help] +on = { fg = "#89b482" } +run = { fg = "#d3869b" } +hovered = { reversed = true, bold = true } +footer = { fg = "#32302f", bg = "#d4be98" } + +# : }}} + + +# : Spotter {{{ + +[spot] +border = { fg = "#7daea3" } +title = { fg = "#7daea3" } +tbl_col = { fg = "#89b482" } +tbl_cell = { fg = "#d3869b", bg = "#45403d" } + +# : }}} + + +# : Notification {{{ + +[notify] +title_info = { fg = "#a9b665" } +title_warn = { fg = "#d8a657" } +title_error = { fg = "#ea6962" } + +# : }}} + + +# : File-specific styles {{{ + +[filetype] + +rules = [ + # Image + { mime = "image/*", fg = "#89b482" }, + # Media + { mime = "{audio,video}/*", fg = "#d8a657" }, + # Archive + { mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", fg = "#d3869b" }, + # Document + { mime = "application/{pdf,doc,rtf}", fg = "#a9b665" }, + # Virtual file system + { mime = "vfs/{absent,stale}", fg = "#a89984" }, + # Fallback + { url = "*", fg = "#d4be98" }, + { url = "*/", fg = "#7daea3" }, +] + +# : }}} + +[icon] + +dirs = [ + { name = ".config", text = "î—Œ", fg = "#d3869b" }, + { name = ".git", text = "", fg = "#89b482" }, + { name = ".github", text = "î—œ", fg = "#7daea3" }, + { name = ".npm", text = "", fg = "#7daea3" }, + { name = "Desktop", text = "", fg = "#89b482" }, + { name = "Development", text = "", fg = "#89b482" }, + { name = "Documents", text = "", fg = "#89b482" }, + { name = "Downloads", text = "", fg = "#89b482" }, + { name = "Library", text = "", fg = "#89b482" }, + { name = "Movies", text = "", fg = "#89b482" }, + { name = "Music", text = "", fg = "#89b482" }, + { name = "Pictures", text = "", fg = "#89b482" }, + { name = "Public", text = "", fg = "#89b482" }, + { name = "Videos", text = "", fg = "#89b482" }, +] +conds = [ + # Special files + { if = "orphan", text = "", fg = "#d4be98" }, + { if = "link", text = "", fg = "#928374" }, + { if = "block", text = "", fg = "#ea6962" }, + { if = "char", text = "", fg = "#ea6962" }, + { if = "fifo", text = "", fg = "#ea6962" }, + { if = "sock", text = "", fg = "#ea6962" }, + { if = "sticky", text = "", fg = "#ea6962" }, + { if = "dummy", text = "", fg = "#ea6962" }, + + # Fallback + { if = "dir", text = "", fg = "#7daea3" }, + { if = "exec", text = "", fg = "#a9b665" }, + { if = "!dir", text = "", fg = "#d4be98" }, +] \ No newline at end of file diff --git a/.config/yazi/flavors/gruvbox-material.yazi/preview.png b/.config/yazi/flavors/gruvbox-material.yazi/preview.png new file mode 100644 index 0000000..bbf67c2 Binary files /dev/null and b/.config/yazi/flavors/gruvbox-material.yazi/preview.png differ diff --git a/.config/yazi/flavors/gruvbox-material.yazi/tmtheme.xml b/.config/yazi/flavors/gruvbox-material.yazi/tmtheme.xml new file mode 100644 index 0000000..37e064d --- /dev/null +++ b/.config/yazi/flavors/gruvbox-material.yazi/tmtheme.xml @@ -0,0 +1,1376 @@ + + + + author + Matthew Dong (github.com/matt-dong-123) + colorSpaceName + sRGB + name + gruvbox-material + semanticClass + gruvbox-material + settings + + + settings + + activeGuide + #363b54 + background + #282828 + caret + #d8a657 + findHighlight + #e78a4e + findHighlightForeground + #282828 + foreground + #d4be98 + guide + #7c6f64 + gutterForeground + #928374 + inactiveSelection + #32302f + invisibles + #7c6f64 + lineHighlight + #32302f + selection + #d3869b + selectionBorder + #d3869b + shadow + #282828 + stackGuide + #7c6f64 + tagsOptions + underline + + + + name + Italics - Comments, Storage, Keyword Flow, Vue attributes, Decorators + scope + comment, meta.var.expr storage.type, keyword.control.flow, meta.directive.vue punctuation.separator.key-value.html, meta.directive.vue entity.other.attribute-name.html, tag.decorator.js entity.name.tag.js, tag.decorator.js punctuation.definition.tag.js, storage.modifier + settings + + fontStyle + italic + + + + name + Comment + scope + comment, comment.block.documentation, punctuation.definition.comment + settings + + foreground + #928374 + + + + name + Comment Doc + scope + comment.block.documentation variable, comment.block.documentation storage, comment.block.documentation punctuation, comment.block.documentation keyword, comment.block.documentation support, comment.block.documentation markup, comment.block.documentation markup.inline.raw.string.markdown, keyword.other.phpdoc.php + settings + + foreground + #928374 + + + + name + Number, Boolean, Undefined, Null + scope + variable.other.constant, punctuation.definition.constant, constant.language, constant.numeric, support.constant + settings + + foreground + #e78a4e + + + + name + String, Symbols, Markup Heading + scope + meta.property.lua,string.unquoted.key.lua,support.other.metaproperty.lua,support.other.metaproperty.lua,constant.other.symbol, constant.other.key, markup.heading, meta.attribute-selector + settings + + fontStyle + + foreground + #89b482 + + + + name + String + scope + string + settings + + fontStyle + + foreground + #a9b665 + + + + name + Colors + scope + constant.other.color, constant.other.color.rgb-value.hex punctuation.definition.constant + settings + + foreground + #7daea3 + + + + name + Info + scope + markup.info + settings + + foreground + #89b482 + background + #32302f + + + + name + Warning + scope + markup.warning + settings + + foreground + #d8a657 + background + #45403d + + + + name + Error + scope + markup.error + settings + + foreground + #ea6962 + background + #45403d + + + + name + Invalid + scope + invalid, invalid.illegal + settings + + foreground + #ea6962 + + + + name + Invalid deprecated + scope + invalid.deprecated + settings + + foreground + #d3869b + + + + name + Storage Type + scope + storage.type + settings + + foreground + #d3869b + + + + name + Storage - modifier, var, const, let + scope + meta.var.expr storage.type, storage.modifier + settings + + foreground + #d3869b + + + + name + Interpolation + scope + punctuation.definition.template-expression, punctuation.section.embedded + settings + + foreground + #7daea3 + + + + name + Spread + scope + keyword.operator.spread, keyword.operator.rest + settings + + fontStyle + bold + foreground + #ea6962 + + + + name + Operator, Misc + scope + keyword.operator, keyword.control.as, keyword.other, keyword.operator.bitwise.shift, punctuation, punctuation.definition.constant.markdown, punctuation.definition.string, punctuation.support.type.property-name, text.html.vue-html meta.tag, punctuation.definition.keyword, punctuation.terminator.rule, punctuation.definition.entity, punctuation.definition.tag, punctuation.separator.inheritance.php, punctuation.definition.tag.html, keyword.other.template, keyword.other.substitution, entity.name.operator, text.html.vue meta.tag.block.any.html, text.html.vue meta.tag.inline.any.html, text.html.vue meta.tag.other.html, text.html.twig meta.tag.inline.any.html, text.html.twig meta.tag.block.any.html, text.html.twig meta.tag.structure.any.html, text.html.twig meta.tag.any.html + settings + + foreground + #89b482 + + + + name + Import, Export, From, Default + scope + keyword.control.import, keyword.control.export, keyword.control.from, keyword.control.default, meta.import keyword.other + settings + + foreground + #7daea3 + + + + name + Keyword + scope + keyword, keyword.control, keyword.other.important + settings + + foreground + #d3869b + + + + name + Keyword SQL + scope + keyword.other.DML + settings + + foreground + #7daea3 + + + + name + Keyword Operator Logical, Arrow, Ternary, Comparison + scope + keyword.operator.logical, storage.type.function, keyword.operator.bitwise, keyword.operator.ternary, keyword.operator.comparison, keyword.operator.relational, keyword.operator.or.regexp + settings + + foreground + #d3869b + + + + name + Tag + scope + entity.name.tag, entity.name.tag support.class.component, meta.tag + settings + + foreground + #ea6962 + + + + name + Tag Punctuation + scope + punctuation.definition.tag, punctuation.definition.tag.html, punctuation.definition.tag.begin.html, punctuation.definition.tag.end.html + settings + + foreground + #d3869b + + + + name + Blade + scope + keyword.blade, entity.name.function.blade + settings + + foreground + #7daea3 + + + + name + PHP - Embedded Tag + scope + punctuation.section.embedded.begin.php, punctuation.section.embedded.end.php + settings + + foreground + #89b482 + + + + name + Smarty - Twig tag - Blade + scope + punctuation.definition.variable.smarty, punctuation.section.embedded.begin.smarty, punctuation.section.embedded.end.smarty, meta.tag.template.value.twig, punctuation.section.tag.twig, meta.tag.expression.twig, punctuation.definition.tag.expression.twig, punctuation.definition.tag.output.twig, variable.parameter.smarty + settings + + foreground + #7daea3 + + + + name + Smarty - Twig variable - function + scope + variable.other.property.twig, support.function.twig, meta.function-call.twig, keyword.control.twig, keyword.control.smarty, keyword.operator.other.twig, keyword.operator.comparison.twig, support.function.functions.twig, support.function.functions.twig, keyword.operator.assignment.twig, support.function.filters.twig, support.function.built-in.smarty, keyword.operator.smarty, text.blade text.html.blade custom.compiler.blade.php punctuation.section.embedded.php entity.name.tag.block.any.html, text.blade text.html.blade custom.compiler.blade.php punctuation.section.embedded.php constant.other.inline-data.html, text.blade text.html.blade custom.compiler.blade.php support.function constant.other.inline-data.html + settings + + foreground + #7daea3 + + + + name + Globals - PHP Constants etc + scope + constant.other.php, variable.other.global.safer, variable.other.global.safer punctuation.definition.variable, variable.other.global, variable.other.global punctuation.definition.variable, constant.other + settings + + foreground + #d8a657 + + + + name + Variables + scope + variable, support.variable, string constant.other.placeholder + settings + + foreground + #d4be98 + + + + name + Object Variable + scope + variable.other.object, support.module.node + settings + + foreground + #d4be98 + + + + name + Object Key + scope + meta.object-literal.key, meta.group.braces.curly constant.other.object.key.js string.unquoted.label.js, string.alias.graphql, string.unquoted.graphql, string.unquoted.alias.graphql, meta.field.declaration.ts variable.object.property + settings + + foreground + #89b482 + + + + name + Object Property + scope + variable.other.property, support.variable.property, support.variable.property.dom, meta.function-call variable.other.object.property, variable.language.prototype, meta.property.object, variable.other.member + settings + + foreground + #7daea3 + + + + name + Object Property + scope + variable.other.object.property + settings + + foreground + #d4be98 + + + + name + Object Literal Member lvl 3 (Vue Prop Validation) + scope + meta.objectliteral meta.object.member meta.objectliteral meta.object.member meta.objectliteral meta.object.member meta.object-literal.key + settings + + foreground + #89b482 + + + + name + C-related Block Level Variables + scope + source.cpp meta.block variable.other + settings + + foreground + #ea6962 + + + + name + Other Variable + scope + support.other.variable + settings + + foreground + #ea6962 + + + + name + Methods + scope + meta.class-method.js entity.name.function.js, entity.name.method.js, variable.function.constructor, keyword.other.special-method, storage.type.cs + settings + + foreground + #7daea3 + + + + name + Function Definition + scope + entity.name.function, meta.function-call, meta.function-call entity.name.function, variable.function, meta.definition.method entity.name.function, meta.object-literal entity.name.function + settings + + foreground + #7daea3 + + + + name + Function Argument + scope + variable.parameter.function.language.special, variable.parameter, meta.function.parameters punctuation.definition.variable, meta.function.parameter variable + settings + + foreground + #d8a657 + + + + name + Constant, Tag Attribute + scope + keyword.other.type.php, storage.type.php, constant.character, constant.escape, keyword.other.unit + settings + + foreground + #d3869b + + + + name + Variable Definition + scope + meta.definition.variable variable.other.constant, meta.definition.variable variable.other.readwrite, variable.other.declaration + settings + + foreground + #d3869b + + + + name + Inherited Class + scope + entity.other.inherited-class + settings + + fontStyle + + foreground + #d3869b + + + + name + Class, Support, DOM, etc + scope + support.class, support.type, variable.other.readwrite.alias, support.orther.namespace.use.php, meta.use.php, support.other.namespace.php, support.type.sys-types, support.variable.dom, support.constant.math, support.type.object.module, support.constant.json, entity.name.namespace, meta.import.qualifier, entity.name.class + settings + + foreground + #89b482 + + + + name + Class Name + scope + entity.name + settings + + foreground + #d4be98 + + + + name + Support Function + scope + support.function + settings + + foreground + #7daea3 + + + + name + CSS Class and Support + scope + source.css support.type.property-name, source.sass support.type.property-name, source.scss support.type.property-name, source.less support.type.property-name, source.stylus support.type.property-name, source.postcss support.type.property-name, support.type.property-name.css, support.type.vendored.property-name, support.type.map.key + settings + + foreground + #7daea3 + + + + name + CSS Font + scope + support.constant.font-name, meta.definition.variable + settings + + foreground + #a9b665 + + + + name + CSS Class + scope + entity.other.attribute-name.class, meta.at-rule.mixin.scss entity.name.function.scss + settings + + foreground + #a9b665 + + + + name + CSS ID + scope + entity.other.attribute-name.id + settings + + foreground + #ea6962 + + + + name + CSS Tag + scope + entity.name.tag.css, entity.name.tag.reference, entity.name.tag.scss + settings + + foreground + #89b482 + + + + name + CSS Tag Reference + scope + entity.name.tag.reference + settings + + foreground + #d8a657 + + + + name + CSS Property Separator + scope + meta.property-list punctuation.separator.key-value + settings + + foreground + #89b482 + + + + name + CSS Punctuation + scope + meta.property-list, punctuation.definition.entity.css + settings + + foreground + #d8a657 + + + + name + SCSS @ + scope + meta.at-rule.mixin keyword.control.at-rule.mixin, meta.at-rule.include entity.name.function.scss, meta.at-rule.include keyword.control.at-rule.include + settings + + foreground + #d3869b + + + + name + SCSS Mixins, Extends, Include Keyword + scope + keyword.control.at-rule.include punctuation.definition.keyword, keyword.control.at-rule.mixin punctuation.definition.keyword, meta.at-rule.include keyword.control.at-rule.include, keyword.control.at-rule.extend punctuation.definition.keyword, meta.at-rule.extend keyword.control.at-rule.extend, entity.other.attribute-name.placeholder.css punctuation.definition.entity.css, meta.at-rule.media keyword.control.at-rule.media, meta.at-rule.mixin keyword.control.at-rule.mixin, meta.at-rule.function keyword.control.at-rule.function, keyword.control punctuation.definition.keyword, meta.at-rule.import.scss entity.other.attribute-name.placeholder.scss punctuation.definition.entity.scss, meta.at-rule.import.scss keyword.control.at-rule.import.scss + settings + + foreground + #d3869b + + + + name + SCSS Include Mixin Argument + scope + meta.property-list meta.at-rule.include + settings + + foreground + #d4be98 + + + + name + CSS value + scope + support.constant.property-value + settings + + foreground + #e78a4e + + + + name + Sub-methods + scope + entity.name.module.js, variable.import.parameter.js, variable.other.class.js + settings + + foreground + #d4be98 + + + + name + Language methods + scope + variable.language + settings + + foreground + #ea6962 + + + + name + Variable punctuation + scope + variable.other punctuation.definition.variable + settings + + foreground + #d4be98 + + + + name + Keyword this with Punctuation, ES7 Bind Operator + scope + source.js constant.other.object.key.js string.unquoted.label.js, variable.language.this punctuation.definition.variable, keyword.other.this + settings + + foreground + #ea6962 + + + + name + HTML Attributes + scope + entity.other.attribute-name, text.html.basic entity.other.attribute-name.html, text.html.basic entity.other.attribute-name, text.blade entity.other.attribute-name.class, text.html.smarty entity.other.attribute-name.class + settings + + foreground + #d3869b + + + + name + Vue Template attributes + scope + meta.directive.vue punctuation.separator.key-value.html, meta.directive.vue entity.other.attribute-name.html + settings + + foreground + #d3869b + + + + name + Vue Template attribute separator + scope + meta.directive.vue punctuation.separator.key-value.html + settings + + foreground + #89b482 + + + + name + CSS IDs + scope + source.sass keyword.control + settings + + foreground + #7daea3 + + + + name + CSS pseudo selectors + scope + entity.other.attribute-name.pseudo-class, entity.other.attribute-name.pseudo-element, entity.other.attribute-name.placeholder, meta.property-list meta.property-value + settings + + foreground + #d3869b + + + + name + Inserted + scope + markup.inserted + settings + + foreground + #a9b665 + + + + name + Deleted + scope + markup.deleted + settings + + foreground + #ea6962 + + + + name + Changed + scope + markup.changed + settings + + foreground + #7daea3 + + + + name + Regular Expressions + scope + string.regexp + settings + + foreground + #89b482 + + + + name + Regular Expressions - Punctuation + scope + punctuation.definition.group + settings + + foreground + #ea6962 + + + + name + Regular Expressions - Character Class + scope + constant.other.character-class.regexp + settings + + foreground + #d3869b + + + + name + Regular Expressions - Character Class Set + scope + constant.other.character-class.set.regexp, punctuation.definition.character-class.regexp + settings + + foreground + #d8a657 + + + + name + Regular Expressions - Quantifier + scope + keyword.operator.quantifier.regexp + settings + + foreground + #89b482 + + + + name + Regular Expressions - Backslash + scope + constant.character.escape.backslash + settings + + foreground + #d4be98 + + + + name + Escape Characters + scope + constant.character.escape + settings + + foreground + #89b482 + + + + name + Decorators + scope + tag.decorator.js entity.name.tag.js, tag.decorator.js punctuation.definition.tag.js + settings + + foreground + #7daea3 + + + + name + CSS Units + scope + keyword.other.unit + settings + + foreground + #ea6962 + + + + name + JSON Key - Level 0 + scope + source.json meta.mapping.key.json string.quoted.double.json, source.json meta.structure.dictionary.json string.quoted.double.json + settings + + foreground + #7daea3 + + + + name + JSON Key - Level 1 + scope + source.json meta.mapping.value.json meta.sequence.json meta.mapping.key.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json string.quoted.double.json + settings + + foreground + #89b482 + + + + name + JSON Key - Level 2 + scope + source.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.mapping.key.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json string.quoted.double.json + settings + + foreground + #7daea3 + + + + name + JSON Key - Level 3 + scope + source.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.key.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json string.quoted.double.json + settings + + foreground + #d3869b + + + + name + JSON Key - Level 4 + scope + source.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.key.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json string.quoted.double.json + settings + + foreground + #d8a657 + + + + name + JSON Key - Level 5 + scope + source.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.key.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json string.quoted.double.json + settings + + foreground + #89b482 + + + + name + JSON Key - Level 6 + scope + source.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.key.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json string.quoted.double.json + settings + + foreground + #89b482 + + + + name + JSON Key - Level 7 + scope + source.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.key.json string.quoted.double.json + settings + + foreground + #ea6962 + + + + name + JSON Key - Level 8 + scope + source.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.value.json meta.sequence.json meta.mapping.key.json string.quoted.double.json punctuation.definition.string.end.json + settings + + foreground + #a9b665 + + + + name + JSON Key - value + scope + source.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json, source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.array.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json + settings + + foreground + #a9b665 + + + + name + Plain Punctuation + scope + punctuation.definition.list_item.markdown + settings + + foreground + #89b482 + + + + name + Block Punctuation + scope + meta.block, meta.brace, punctuation.definition.block, punctuation.definition.use, punctuation.definition.group.shell, punctuation.definition.class, punctuation.definition.begin.bracket, punctuation.definition.end.bracket, punctuation.definition.parameters, punctuation.definition.arguments, punctuation.definition.dictionary, punctuation.definition.array, punctuation.section + settings + + foreground + #89b482 + + + + name + Markdown - Plain + scope + meta.jsx.children, meta.embedded.block + settings + + foreground + #d4be98 + + + + name + HTML text + scope + text.html + settings + + foreground + #7daea3 + + + + name + Markdown - Markup Raw Inline + scope + text.html.markdown markup.inline.raw.markdown + settings + + foreground + #d3869b + + + + name + Markdown - Markup Raw Inline Punctuation + scope + text.html.markdown markup.inline.raw.markdown punctuation.definition.raw.markdown + settings + + foreground + #7c6f64 + + + + name + Markdown - Heading 1 + scope + heading.1.markdown entity.name, heading.1.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #89b482 + + + + name + Markdown - Heading 2 + scope + heading.2.markdown entity.name, heading.2.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #89b482 + + + + name + Markdown - Heading 3 + scope + heading.3.markdown entity.name, heading.3.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #7daea3 + + + + name + Markdown - Heading 4 + scope + heading.4.markdown entity.name, heading.4.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #7daea3 + + + + name + Markdown - Heading 5 + scope + heading.5.markdown entity.name, heading.5.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #7daea3 + + + + name + Markdown - Heading 6 + scope + heading.6.markdown entity.name, heading.6.markdown punctuation.definition.heading.markdown + settings + + fontStyle + bold + foreground + #a89984 + + + + name + Markup - Italic + scope + markup.italic, markup.italic punctuation + settings + + fontStyle + italic + foreground + #d4be98 + + + + name + Markup - Bold + scope + markup.bold, markup.bold punctuation + settings + + fontStyle + bold + foreground + #d4be98 + + + + name + Markup - Bold-Italic + scope + markup.bold markup.italic, markup.bold markup.italic punctuation + settings + + fontStyle + bold italic + foreground + #d4be98 + + + + name + Markup - Underline + scope + markup.underline, markup.underline punctuation + settings + + fontStyle + underline + + + + name + Markdown - Blockquote + scope + markup.quote punctuation.definition.blockquote.markdown + settings + + foreground + #7c6f64 + + + + name + Markup - Quote + scope + markup.quote + settings + + fontStyle + italic + + + + name + Markdown - Link + scope + string.other.link, markup.underline.link, constant.other.reference.link.markdown, string.other.link.description.title.markdown + settings + + foreground + #89b482 + + + + name + Markdown - Fenced Code Block + scope + markup.fenced_code.block.markdown, markup.inline.raw.string.markdown, variable.language.fenced.markdown + settings + + foreground + #89b482 + + + + name + Markdown - Separator + scope + meta.separator + settings + + fontStyle + bold + foreground + #7c6f64 + + + + name + Markup - Table + scope + markup.table + settings + + foreground + #d4be98 + + + + name + Token - Info + scope + token.info-token + settings + + foreground + #89b482 + + + + name + Token - Warn + scope + token.warn-token + settings + + foreground + #d8a657 + + + + name + Token - Error + scope + token.error-token + settings + + foreground + #ea6962 + + + + name + Token - Debug + scope + token.debug-token + settings + + foreground + #d3869b + + + + name + Apache Tag + scope + entity.tag.apacheconf + settings + + foreground + #ea6962 + + + + name + Preprocessor + scope + meta.preprocessor + settings + + foreground + #89b482 + + + + name + ENV value + scope + source.env + settings + + foreground + #7daea3 + + + + uuid + 06f855e3-9fb7-4fb1-b790-aef06065f34e + + diff --git a/.config/yazi/init.lua b/.config/yazi/init.lua new file mode 100644 index 0000000..c9b48fc --- /dev/null +++ b/.config/yazi/init.lua @@ -0,0 +1,11 @@ +require("full-border"):setup({ + -- Available values: ui.Border.PLAIN, ui.Border.ROUNDED + type = ui.Border.ROUNDED, +}) + +require("git"):setup({ + -- Order of status signs showing in the linemode + order = 1500, +}) + +-- require("relative-motions"):setup({ show_numbers = "relative", show_motion = true, enter_mode = "first" }) diff --git a/.config/yazi/keymap.toml b/.config/yazi/keymap.toml new file mode 100644 index 0000000..ae4495f --- /dev/null +++ b/.config/yazi/keymap.toml @@ -0,0 +1,56 @@ +[[mgr.prepend_keymap]] +on = "M" +run = "plugin mount" + +# [[mgr.prepend_keymap]] +# on = [ "m" ] +# run = "plugin relative-motions" +# desc = "Trigger a new relative motion" + +[[mgr.prepend_keymap]] +on = "f" +run = "plugin jump-to-char" +desc = "Jump to char" + + +[[mgr.prepend_keymap]] +on = "F" +run = "filter --smart" +desc = "Filter files" + + +[[mgr.prepend_keymap]] +on = "l" +run = "plugin smart-enter" +desc = "Enter the child directory, or open the file" + +[[mgr.prepend_keymap]] +on = [ "C" ] +run = "plugin chmod" +desc = "Chmod on selected files" + +[[mgr.prepend_keymap]] +on = "+" +run = "plugin zoom 1" +desc = "Zoom in hovered file" + +[[mgr.prepend_keymap]] +on = "-" +run = "plugin zoom -1" +desc = "Zoom out hovered file" + +# [[mgr.prepend_keymap]] +# on = "o" +# run = "plugin open-with-cmd -- block" +# desc = "Open with command in the terminal" + +[[mgr.prepend_keymap]] +on = "L" +run = "plugin open-with-cmd" +desc = "Open with command" + +[[mgr.prepend_keymap]] +on = "" +run = "plugin diff" +desc = "Diff the selected with the hovered file" + diff --git a/.config/yazi/package.toml b/.config/yazi/package.toml new file mode 100644 index 0000000..21f35ee --- /dev/null +++ b/.config/yazi/package.toml @@ -0,0 +1,69 @@ +[[plugin.deps]] +use = "yazi-rs/plugins:mount" +rev = "598cdb6" +hash = "e3260d0b1feded8c8e3ae3afa1169e30" + +[[plugin.deps]] +use = "yazi-rs/plugins:smart-enter" +rev = "598cdb6" +hash = "187cc58ba7ac3befd49c342129e6f1b6" + +[[plugin.deps]] +use = "yazi-rs/plugins:full-border" +rev = "598cdb6" +hash = "7b625412411be411153886894d9acaf" + +[[plugin.deps]] +use = "yazi-rs/plugins:git" +rev = "598cdb6" +hash = "88e56a64b7ce7c4314427452343fef17" + +[[plugin.deps]] +use = "yazi-rs/plugins:chmod" +rev = "598cdb6" +hash = "f0c8c378184d5f8abd1b095a443d336d" + +[[plugin.deps]] +use = "yazi-rs/plugins:zoom" +rev = "598cdb6" +hash = "fa268cb59989a87780605fdcfb8d99a9" + +[[plugin.deps]] +use = "yazi-rs/plugins:piper" +rev = "598cdb6" +hash = "d99cfbc3812e7738505f9e4bedb759cd" + +[[plugin.deps]] +use = "boydaihungst/gvfs" +rev = "3abc0a2" +hash = "eed89e75671723c8f9390a2862af0e4e" + +[[plugin.deps]] +use = "Ape/open-with-cmd" +rev = "eba191d" +hash = "722a60eca1b338c9c24bb508d2d48578" + +[[plugin.deps]] +use = "yazi-rs/plugins:diff" +rev = "598cdb6" +hash = "8b1af6b5a69797ee951f2a80ce570818" + +[[plugin.deps]] +use = "dedukun/relative-motions" +rev = "a603d9e" +hash = "e02a788e5b8ae0fb47fd0193dda589cc" + +[[plugin.deps]] +use = "yazi-rs/plugins:jump-to-char" +rev = "598cdb6" +hash = "7a4b4237223aaa94e589d1c01a23542a" + +[[flavor.deps]] +use = "Chromium-3-Oxide/everforest-medium" +rev = "e1ead7b" +hash = "f7dc46200e53da5e94858630184a9220" + +[[flavor.deps]] +use = "matt-dong-123/gruvbox-material" +rev = "e0fd2d8" +hash = "8f46ca96c0bbe1bdfb4fa201dd5eed4b" diff --git a/.config/yazi/plugins/chmod.yazi/LICENSE b/.config/yazi/plugins/chmod.yazi/LICENSE new file mode 100644 index 0000000..fb5b1d6 --- /dev/null +++ b/.config/yazi/plugins/chmod.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/.config/yazi/plugins/chmod.yazi/README.md b/.config/yazi/plugins/chmod.yazi/README.md new file mode 100644 index 0000000..5a73207 --- /dev/null +++ b/.config/yazi/plugins/chmod.yazi/README.md @@ -0,0 +1,28 @@ +# chmod.yazi + +Execute `chmod` on the selected files to change their mode. This plugin is only available on Unix platforms since it relies on [`chmod(2)`](https://man7.org/linux/man-pages/man2/chmod.2.html). + +https://github.com/yazi-rs/plugins/assets/17523360/7aa3abc2-d057-498c-8473-a6282c59c464 + +## Installation + +```sh +ya pkg add yazi-rs/plugins:chmod +``` + +## Usage + +Add this to your `~/.config/yazi/keymap.toml`: + +```toml +[[mgr.prepend_keymap]] +on = [ "c", "m" ] +run = "plugin chmod" +desc = "Chmod on selected files" +``` + +Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other actions/plugins. + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/.config/yazi/plugins/chmod.yazi/main.lua b/.config/yazi/plugins/chmod.yazi/main.lua new file mode 100644 index 0000000..87af0bf --- /dev/null +++ b/.config/yazi/plugins/chmod.yazi/main.lua @@ -0,0 +1,47 @@ +--- @since 26.1.22 + +local selected_or_hovered = ya.sync(function() + local tab, paths = cx.active, {} + for _, u in pairs(tab.selected) do + paths[#paths + 1] = tostring(u) + end + if #paths == 0 and tab.current.hovered then + paths[1] = tostring(tab.current.hovered.url) + end + return paths +end) + +local function fail(s, ...) + ya.notify { + title = "Chmod", + content = string.format(s, ...), + level = "error", + timeout = 5, + } +end + +return { + entry = function() + ya.emit("escape", { visual = true }) + + local urls = selected_or_hovered() + if #urls == 0 then + return ya.notify { title = "Chmod", content = "No file selected", level = "warn", timeout = 5 } + end + + local value, event = ya.input { + title = "Chmod:", + pos = { "top-center", y = 3, w = 40 }, + } + if event ~= 1 then + return + end + + local output, err = Command("chmod"):arg(value):arg(urls):output() + if not output then + fail("Failed to run chmod: %s", err) + elseif not output.status.success then + fail("Chmod failed with stderr:\n%s", output.stderr:gsub("^chmod:%s*", "")) + end + end, +} diff --git a/.config/yazi/plugins/diff.yazi/LICENSE b/.config/yazi/plugins/diff.yazi/LICENSE new file mode 100644 index 0000000..fb5b1d6 --- /dev/null +++ b/.config/yazi/plugins/diff.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/.config/yazi/plugins/diff.yazi/README.md b/.config/yazi/plugins/diff.yazi/README.md new file mode 100644 index 0000000..ea9918f --- /dev/null +++ b/.config/yazi/plugins/diff.yazi/README.md @@ -0,0 +1,28 @@ +# diff.yazi + +Diff the selected file with the hovered file, create a living patch, and copy it to the clipboard. + +https://github.com/yazi-rs/plugins/assets/17523360/eff5e949-386a-44ea-82f9-4cb4a2c37aad + +## Installation + +```sh +ya pkg add yazi-rs/plugins:diff +``` + +## Usage + +Add this to your `~/.config/yazi/keymap.toml`: + +```toml +[[mgr.prepend_keymap]] +on = "" +run = "plugin diff" +desc = "Diff the selected with the hovered file" +``` + +Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other actions/plugins. + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/.config/yazi/plugins/diff.yazi/main.lua b/.config/yazi/plugins/diff.yazi/main.lua new file mode 100644 index 0000000..a840767 --- /dev/null +++ b/.config/yazi/plugins/diff.yazi/main.lua @@ -0,0 +1,41 @@ +--- @since 26.1.22 + +local function info(content) + return ya.notify { + title = "Diff", + content = content, + timeout = 5, + } +end + +local selected_path = ya.sync(function() + for _, u in pairs(cx.active.selected) do + return u.cache or u + end +end) + +local hovered_path = ya.sync(function() + local h = cx.active.current.hovered + return h and h.path +end) + +return { + entry = function() + local a, b = selected_path(), hovered_path() + if not a then + return info("No file selected") + elseif not b then + return info("No file hovered") + end + + local output, err = Command("diff"):arg("-Naur"):arg(tostring(a)):arg(tostring(b)):output() + if not output then + return info("Failed to run diff, error: " .. err) + elseif output.stdout == "" then + return info("No differences found") + end + + ya.clipboard(output.stdout) + info("Diff copied to clipboard") + end, +} diff --git a/.config/yazi/plugins/full-border.yazi/LICENSE b/.config/yazi/plugins/full-border.yazi/LICENSE new file mode 100644 index 0000000..fb5b1d6 --- /dev/null +++ b/.config/yazi/plugins/full-border.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/.config/yazi/plugins/full-border.yazi/README.md b/.config/yazi/plugins/full-border.yazi/README.md new file mode 100644 index 0000000..269ca8e --- /dev/null +++ b/.config/yazi/plugins/full-border.yazi/README.md @@ -0,0 +1,32 @@ +# full-border.yazi + +Add a full border to Yazi to make it look fancier. + +![full-border](https://github.com/yazi-rs/plugins/assets/17523360/ef81b560-2465-4d36-abf2-5d21dcb7b987) + +## Installation + +```sh +ya pkg add yazi-rs/plugins:full-border +``` + +## Usage + +Add this to your `init.lua` to enable the plugin: + +```lua +require("full-border"):setup() +``` + +Or you can customize the border type: + +```lua +require("full-border"):setup { + -- Available values: ui.Border.PLAIN, ui.Border.ROUNDED + type = ui.Border.ROUNDED, +} +``` + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/.config/yazi/plugins/full-border.yazi/main.lua b/.config/yazi/plugins/full-border.yazi/main.lua new file mode 100644 index 0000000..1b850c2 --- /dev/null +++ b/.config/yazi/plugins/full-border.yazi/main.lua @@ -0,0 +1,44 @@ +--- @since 26.5.6 + +local function setup(_, opts) + local type = opts and opts.type or ui.Border.ROUNDED + local old_build = Tab.build + + Tab.build = function(self, ...) + local bar = function(c, x, y) + if x <= 0 or x == self._area.w - 1 or th.mgr.border_symbol ~= "│" then + return ui.Bar(ui.Edge.TOP) + end + + return ui.Bar(ui.Edge.TOP) + :area(ui.Rect { + x = x, + y = math.max(0, y), + w = ya.clamp(0, self._area.w - x, 1), + h = math.min(1, self._area.h), + }) + :symbol(c) + end + + local c = self._chunks + self._chunks = { + c[1]:pad(ui.Pad.y(1)), + c[2]:pad(ui.Pad.y(1)), + c[3]:pad(ui.Pad.y(1)), + } + + local style = th.mgr.border_style + self._base = ya.list_merge(self._base or {}, { + ui.Border(ui.Edge.ALL):area(self._area):type(type):style(style), + + bar("┬", c[2].x, c[1].y), + bar("┮", c[2].x, c[1].bottom - 1), + bar("┬", c[2].right - 1, c[2].y), + bar("┮", c[2].right - 1, c[2].bottom - 1), + }) + + old_build(self, ...) + end +end + +return { setup = setup } diff --git a/.config/yazi/plugins/git.yazi/LICENSE b/.config/yazi/plugins/git.yazi/LICENSE new file mode 100644 index 0000000..fb5b1d6 --- /dev/null +++ b/.config/yazi/plugins/git.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/.config/yazi/plugins/git.yazi/README.md b/.config/yazi/plugins/git.yazi/README.md new file mode 100644 index 0000000..bb923b0 --- /dev/null +++ b/.config/yazi/plugins/git.yazi/README.md @@ -0,0 +1,86 @@ +# git.yazi + +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 pkg add yazi-rs/plugins:git +``` + +## Setup + +Add the following to your `~/.config/yazi/init.lua`: + +```lua +require("git"):setup { + -- Order of status signs showing in the linemode + order = 1500, +} +``` + +And register it as fetchers in your `~/.config/yazi/yazi.toml`: + +```toml +[[plugin.prepend_fetchers]] +id = "git" # Remove if Yazi > v26.1.22 +url = "*" +run = "git" +group = "git" + +[[plugin.prepend_fetchers]] +id = "git" # Remove if Yazi > v26.1.22 +url = "*/" +run = "git" +group = "git" +``` + +## Advanced + +You can customize the [Style](https://yazi-rs.github.io/docs/configuration/theme#types.style) of the status sign with: + +- `[git].unknown` - status cannot/not yet determined +- `[git].modified` - modified file +- `[git].added` - added file +- `[git].untracked` - untracked file +- `[git].ignored` - ignored file +- `[git].deleted` - deleted file +- `[git].updated` - updated file +- `[git].clean` - clean file + +For example: + +```toml +# theme.toml / flavor.toml +[git] +modified = { fg = "blue" } +deleted = { fg = "red", bold = true } +``` + +You can also customize the text of the status sign with: + +- `[git].unknown_sign` - status cannot/not yet determined +- `[git].modified_sign` - modified file +- `[git].added_sign` - added file +- `[git].untracked_sign` - untracked file +- `[git].ignored_sign` - ignored file +- `[git].deleted_sign` - deleted file +- `[git].updated_sign` - updated file +- `[git].clean_sign` - clean file + +For example: + +```toml +# theme.toml / flavor.toml +[git] +unknown_sign = " " +modified_sign = "M" +deleted_sign = "D" +clean_sign = "✔" +``` + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/.config/yazi/plugins/git.yazi/main.lua b/.config/yazi/plugins/git.yazi/main.lua new file mode 100644 index 0000000..19f5c44 --- /dev/null +++ b/.config/yazi/plugins/git.yazi/main.lua @@ -0,0 +1,259 @@ +--- @since 26.5.6 + +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` +---@enum CODES +local CODES = { + unknown = 100, -- status cannot/not yet determined + excluded = 99, -- ignored directory + ignored = 6, -- ignored file + untracked = 5, + modified = 4, + added = 3, + deleted = 2, + updated = 1, + clean = 0, +} + +local PATTERNS = { + { "!$", CODES.ignored }, + { "?$", CODES.untracked }, + { "[MT]", CODES.modified }, + { "[AC]", CODES.added }, + { "D", CODES.deleted }, + { "U", CODES.updated }, + { "[AD][AD]", CODES.updated }, +} + +---@param line string +---@return CODES, string +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 + ---@diagnostic disable-next-line: missing-return + end +end + +---@param cwd Url +---@return string? +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 + +---@param changed Changes +---@return Changes +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.clean) > code and new[s] or code + url = url.parent + end + end + end + return new +end + +---@param excluded string[] +---@param cwd Url +---@param repo Url +---@return Changes +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 + +---@param cwd string +---@param repo string +---@param changed Changes +local add = ya.sync(function(st, cwd, repo, changed) + ---@cast st State + + st.dirs[cwd] = repo + st.repos[repo] = st.repos[repo] or {} + for path, code in pairs(changed) do + if code == CODES.clean 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 + ui.render() +end) + +---@param cwd string +local remove = ya.sync(function(st, cwd) + ---@cast st State + + local repo = st.dirs[cwd] + if not repo then + return + end + + ui.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) + +---@param st State +---@param opts Options +local function setup(st, opts) + st.dirs = {} + st.repos = {} + + opts = opts or {} + opts.order = opts.order or 1500 + + local t = th.git or {} + local styles = { + [CODES.unknown] = t.unknown or ui.Style(), + [CODES.ignored] = t.ignored or ui.Style():fg("darkgray"), + [CODES.untracked] = t.untracked or ui.Style():fg("magenta"), + [CODES.modified] = t.modified or ui.Style():fg("yellow"), + [CODES.added] = t.added or ui.Style():fg("green"), + [CODES.deleted] = t.deleted or ui.Style():fg("red"), + [CODES.updated] = t.updated or ui.Style():fg("yellow"), + [CODES.clean] = t.clean or ui.Style(), + } + local signs = { + [CODES.unknown] = t.unknown_sign or "", + [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 " ", + [CODES.clean] = t.clean_sign or "", + } + + Linemode:children_add(function(self) + if not self._file.in_current then + return "" + end + + local url = self._file.url + local repo = st.dirs[tostring(url.base or url.parent)] + local code = CODES.unknown + if repo then + code = repo == CODES.excluded and CODES.ignored or st.repos[repo][tostring(url):sub(#repo + 2)] or CODES.clean + end + + if 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 + +---@type UnstableFetcher +local function fetch(_, job) + local cwd = job.files[1].url.base or job.files[1].url.parent + 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)) + :arg({ "--no-optional-locks", "-c", "core.quotePath=", "status", "--porcelain", "-unormal", "--no-renames", "--ignored=matching" }) + :arg(paths) + :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 `clean`, + -- 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.clean + end + + add(tostring(cwd), repo, changed) + + return false +end + +return { setup = setup, fetch = fetch } diff --git a/.config/yazi/plugins/git.yazi/types.lua b/.config/yazi/plugins/git.yazi/types.lua new file mode 100644 index 0000000..9936849 --- /dev/null +++ b/.config/yazi/plugins/git.yazi/types.lua @@ -0,0 +1,12 @@ +---@class State +---@field dirs table Mapping between a directory and its corresponding repository +---@field repos table Mapping between a repository and the status of each of its files + +---@class Options +---@field order number The order in which the status is displayed +---@field renamed boolean Whether to include renamed files in the status (or treat them as modified) + +-- TODO: move this to `types.yazi` once it's get stable +---@alias UnstableFetcher fun(self: unknown, job: { files: File[] }): boolean, Error? + +---@alias Changes table diff --git a/.config/yazi/plugins/gvfs.yazi/LICENSE b/.config/yazi/plugins/gvfs.yazi/LICENSE new file mode 100644 index 0000000..402ca9b --- /dev/null +++ b/.config/yazi/plugins/gvfs.yazi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 boydaihungst + +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/.config/yazi/plugins/gvfs.yazi/README.md b/.config/yazi/plugins/gvfs.yazi/README.md new file mode 100644 index 0000000..c6312aa --- /dev/null +++ b/.config/yazi/plugins/gvfs.yazi/README.md @@ -0,0 +1,324 @@ +# gvfs.yazi + + + +- [Preview](#preview) +- [Features](#features) +- [Requirements](#requirements) +- [Installation](#installation) +- [Usage](#usage) +- [Note for mounting using fstab](#note-for-mounting-using-fstab) +- [Troubleshooting](#troubleshooting) + + + +[gvfs.yazi](https://github.com/boydaihungst/gvfs.yazi) uses [gvfs](https://wiki.gnome.org/Projects/gvfs) and [gio from glib](https://github.com/GNOME/glib) to transparently mount and unmount devices or remote storage in read and write mode, +allowing you to navigate inside, view, and edit individual or groups of files and folders. + +Supported protocols: MTP, Hard disk/drive, SMB, SFTP, NFS, GPhoto2 (PTP), FTP, Google Drive (via [GOA](./GNOME_ONLINE_ACCOUNTS_GOA.md)), One drive (via [GOA](./GNOME_ONLINE_ACCOUNTS_GOA.md)), DNS-SD, DAV (WebDAV), AFP, AFC. You need to install corresponding packages to use them. + +Tested: MTP, Hard disk/drive (Encrypted and Unencrypted), GPhoto2 (PTP), DAV, SFTP, FTP, SMB, NFSv4, Google Drive, One Drive. You may need to unlock and turn screen on to mount some devices (Android MTP, etc.) + +> [!NOTE] +> +> - This plugin only supports Linux + +## Preview + +https://github.com/user-attachments/assets/6aad98f7-081a-4e06-b398-5f7e8ca4ab39 + +Google-drive: + +https://github.com/user-attachments/assets/fb74a710-5f05-4bf4-b95f-10f40583c5a0 + +## Features + +- Supports all gvfs schemes/protocols (mtp, smb, ftp, sftp, nfs, gphoto2, afp, afc, sshfs, dav, davs, dav+sd, davs+sd, dns-sd) +- Mount hardware device or saved scheme/mount URI (use `--select-then-mount`) +- Auto-jump to mounted location after mount (use `select-then-mount --jump`) +- Unmount and eject hardware devices (use `select-then-unmount --eject`) +- Auto select the first device or saved scheme/mount URI if there is only one listed. +- Jump to mounted location (use `jump-to-device`) +- After jumped to mounted location, jump back to the previous location + with a single keybind. Make it easier to copy/paste files. (use `jump-back-prev-cwd`) +- Add/Edit/Remove scheme/mount URI (use `add-mount`, `edit-mount`, `remove-mount`). Check this for schemes/mount URI format: [schemes.html]() +- (Optional) Remember passwords using Keyring or Password Store (need `secret-tool` + `keyring` or `pass` + `gpg` installed) + +## Requirements + +1. [yazi >= 25.5.31](https://github.com/sxyazi/yazi) + +2. This plugin only supports Linux, and requires having [GLib](https://github.com/GNOME/glib), [gvfs](https://gitlab.gnome.org/GNOME/gvfs) (need D-Bus Session) + + ```sh + # Ubuntu + sudo apt install gvfs libglib2.0-dev + + # Fedora + sudo dnf install gvfs gvfs-fuse + + # Arch + sudo pacman -S gvfs glib2 + + # Gentoo (Use elogind (openrc) or systemd) + # Add fuse USE flag to /etc/portage/package.use/gvfs.conf: + gnome-base/gvfs fuse + # Then run this to install gvfs: + sudo emerge -av gnome-base/gvfs + ``` + +3. And other `gvfs` protocol packages, choose what you need, all of them are optional: + + ```sh + # Ubuntu + # This included all protocols + sudo apt install gvfs-backends gvfs-libs + + # Fedora + sudo dnf install gvfs-afc gvfs-afp gvfs-archive gvfs-goa gvfs-gphoto2 gvfs-mtp gvfs-nfs gvfs-smb + + # Arch + sudo pacman -S gvfs-mtp gvfs-afc gvfs-google gvfs-gphoto2 gvfs-nfs gvfs-smb gvfs-afc gvfs-dnssd gvfs-goa gvfs-onedrive gvfs-wsdd + + # Gentoo + # Edit /etc/portage/package.use/gvfs.conf again, add more USE flags: + # https://wiki.gentoo.org/wiki/GVfs + gnome-base/gvfs afp gnome-online-accounts google ios mtp nfs onedrive samba zeroconf fuse gphoto2 + # Then run this to update gvfs: + sudo emerge -av gnome-base/gvfs + ``` + +4. For headless session (non-active console, like connect to a computer via SSH, etc.) + If you see `GVFS.yazi can only run on DBUS session` error message, please refer to [HEADLESS_WORKAROUND.md](./HEADLESS_WORKAROUND.md) for a workaround. + +5. (Optional) Store passwords with Keyring or Password Store (secret-tool + keyring or pass + gpg) + There are two methods to securely store passwords. Please refer to [SECURE_SAVED_PASSWORD.md](./SECURE_SAVED_PASSWORD.md) for more information. + +6. Restart to make sure gvfs deamon is started. + +## Installation + +```sh +ya pkg add boydaihungst/gvfs +``` + +Modify your `~/.config/yazi/init.lua` to include (`setup` function is required): + +```lua +require("gvfs"):setup({ + -- (Optional) Allowed keys to select device. + which_keys = "1234567890qwertyuiopasdfghjklzxcvbnm-=[]\\;',./!@#$%^&*()_+{}|:\"<>?", + + -- (Optional) Table of blacklisted devices. These devices will be ignored in any actions + -- List of device properties to match, or a string to match the device name: + -- https://github.com/boydaihungst/gvfs.yazi/blob/master/main.lua#L144 + blacklist_devices = { { name = "Wireless Device", scheme = "mtp" }, { scheme = "file" }, "Device Name"}, + + -- (Optional) Save file. + -- Default: ~/.config/yazi/gvfs.private + save_path = os.getenv("HOME") .. "/.config/yazi/gvfs.private", + + -- (Optional) Save file for automount devices. Use with `automount-when-cd` action. + -- Default: ~/.config/yazi/gvfs_automounts.private + save_path_automounts = os.getenv("HOME") .. "/.config/yazi/gvfs_automounts.private", + + -- (Optional) Input box position. + -- Default: { "top-center", y = 3, w = 60 }, + -- Position, which is a table: + -- `1`: Origin position, available values: "top-left", "top-center", "top-right", + -- "bottom-left", "bottom-center", "bottom-right", "center", and "hovered". + -- "hovered" is the position of hovered file/folder + -- `x`: X offset from the origin position. + -- `y`: Y offset from the origin position. + -- `w`: Width of the input. + -- `h`: Height of the input. + input_position = { "center", y = 0, w = 60 }, + + -- (Optional) Select where to save passwords. + -- Default: nil + -- Available options: "keyring", "pass", or nil + password_vault = "keyring", + + -- (Optional) Only need if you set password_vault = "pass" + -- Read the guide at SECURE_SAVED_PASSWORD.md to get your key_grip + key_grip = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + + -- (Optional) Auto-save password after mount. + -- Default: false + save_password_autoconfirm = true, + -- (Optional) mountpoint of gvfs. Default: /run/user/USER_ID/gvfs + -- On some system it could be ~/.gvfs + -- You can't decide this path, it will be created automatically. Only changed if you know where gvfs mountpoint is. + -- Use command `ps aux | grep gvfs` to search for gvfs process and get the mountpoint path. + -- root_mountpoint = (os.getenv("XDG_RUNTIME_DIR") or ("/run/user/" .. ya.uid())) .. "/gvfs" +}) +``` + +## Usage + +> [!NOTE] +> +> - Moving files to the Trash bin does not work with some protocols (e.g., Android MTP). Please use permanent delete instead. +> - Scheme/Mount URIs shouldn't contain password, because they are saved as plain text in `yazi/config/gvfs.private`. +> - Google Drive, One drive are created via GNOME Online Accounts (GOA). +> Guide to setup [GNOME_ONLINE_ACCOUNTS_GOA.md](./GNOME_ONLINE_ACCOUNTS_GOA.md) +> - MTP, GPhoto2, AFC, Hard disk/drive, fstab with x-gvfs-show mount option, Google Drive + One drive protocols are listed automatically. So you don't need to add them via `add-mount` command. +> For other protocols (smb, ftp, sftp, etc), use `add-mount` command to add [Schemes/Mount URI](). +> - There is a bug in Yazi that prevents mounted folders from refreshing after they are mounted/unmounted. +> If you encounter this issue, try opening a new tab or moving the cursor up and down to trigger a refresh. +> - Mount Windows bitlocker drives requires bitlocker password to unlock: https://aka.ms/myrecoverykey or https://support.microsoft.com/en-us/windows/find-your-bitlocker-recovery-key-6b71ad27-0b89-ea08-f143-056f5ab347d6 +> - Mount hard drives may require polkit rule to fix permission denied error. Refer to [HEADLESS_WORKAROUND.md](./HEADLESS_WORKAROUND.md) for a workaround. + +Add this to your `~/.config/yazi/keymap.toml`: + +```toml +[mgr] +prepend_keymap = [ + # Mount + { on = [ "M", "m" ], run = "plugin gvfs -- select-then-mount", desc = "Select device then mount" }, + # or this if you want to jump to mountpoint after mounted + { on = [ "M", "m" ], run = "plugin gvfs -- select-then-mount --jump", desc = "Select device to mount and jump to its mount point" }, + + # This will remount device under current working directory (cwd) + # -> cwd = /run/user/1000/gvfs/DEVICE_1/FOLDER_A + # -> device mountpoint = /run/user/1000/gvfs/DEVICE_1 + # -> remount this DEVIEC_1 if needed + { on = [ "M", "R" ], run = "plugin gvfs -- remount-current-cwd-device", desc = "Remount device under cwd" }, + + { on = [ "M", "u" ], run = "plugin gvfs -- select-then-unmount", desc = "Select device then unmount" }, + # Or this if you want to unmount and eject device. + # -> Ejected device can safely be removed. + # -> Ejecting a device will unmount all paritions/volumes under it. + # -> Fallback to normal unmount if not supported by device. + { on = [ "M", "u" ], run = "plugin gvfs -- select-then-unmount --eject", desc = "Select device then eject" }, + + # Also support force unmount/eject. + # -> Ignore outstanding file operations when unmounting or ejecting + { on = [ "M", "U" ], run = "plugin gvfs -- select-then-unmount --eject --force", desc = "Select device then force to eject/unmount" }, + + # Add Scheme/Mount URI: + # -> Available schemes: mtp, gphoto2, smb, sftp, ftp, nfs, dns-sd, dav, davs, dav+sd, davs+sd, afp, afc, sshfs + # -> Read more about the schemes here: https://wiki.gnome.org/Projects(2f)gvfs(2f)schemes.html + # -> Explain about the scheme: + # -> If it shows like this: {ftp,ftps,ftpis}://[user@]host[:port] + # -> All of the value within [] is optional. For values within {}, you must choose exactly one. All others are required. + # -> empty [user] or "anonymous" user is anonymous user in (ftp) + # -> ftp://anonymous@192.168.1.2:9999 -> skip user input step. + # -> ftp://192.168.1.2:9999 -> input empty value in user input box. + # -> Example: {ftp,ftps,ftpis}://[user@]host[:port] => ip and port: "ftp://myusername@192.168.1.2:9999" or domain: "ftps://myusername@github.com" + # -> More examples: smb://user@192.168.1.2/share, smb://WORKGROUP;user@192.168.1.2/share, sftp://user@192.168.1.2/, ftp://192.168.1.2/ + # !WARNING: - Scheme/Mount URI shouldn't contain password. + # - Google Drive, One drive are listed automatically via GNOME Online Accounts (GOA). Avoid adding them. + # - MTP, GPhoto2, AFC, Hard disk/drive, fstab with x-gvfs-show are also listed automatically. Avoid adding them. + # - SSH, SFTP, FTP(s), AFC, DNS_SD now support [/share]. For example: sftp://user@192.168.1.2/home/user_name -> /share = /home/user_name + # - ssh:// is alias for sftp://. + # -> {sftp,ssh}://[user@]host[:port]. Host can be Host alias in .ssh/config file, ip or domain. + # -> For example (home is Host alias in .ssh/config file: Host home): + # -> ssh://user_name@home/home/user_name -> this will mount root path, but jump to subfolder /home/user_name + # -> sftp://user_name@192.168.1.2/home/user_name -> same as above but with ip + # -> sftp://user_name@192.168.1.2:9999/home/user_name -> same as above but with ip and port + { on = [ "M", "a" ], run = "plugin gvfs -- add-mount", desc = "Add a GVFS mount URI" }, + + # Edit a Scheme/Mount URI + # -> Will clear saved passwords for that mount URI. + { on = [ "M", "e" ], run = "plugin gvfs -- edit-mount", desc = "Edit a GVFS mount URI" }, + + # Remove a Scheme/Mount URI + # -> Will clear saved passwords for that mount URI. + { on = [ "M", "r" ], run = "plugin gvfs -- remove-mount", desc = "Remove a GVFS mount URI" }, + + # Jump + { on = [ "g", "m" ], run = "plugin gvfs -- jump-to-device", desc = "Select device then jump to its mount point" }, + # If you use `x-systemd.automount` in /etc/fstab or manually added automount unit, + # then you can use `--automount` argument to auto mount device before jump. + # Otherwise it won't show up in the jump list. + { on = [ "g", "m" ], run = "plugin gvfs -- jump-to-device --automount", desc = "Automount then select device to jump to its mount point" }, + { on = [ "`", "`" ], run = "plugin gvfs -- jump-back-prev-cwd", desc = "Jump back to the position before jumped to device" }, + + # Automount (This is different from `x-systemd.automount` in /etc/fstab) + # -> Hover over any file/folder under a mounted device then run `automount-when-cd` action to enable automount when cd/jump for that device. + # -> When you cd/jump to unmounted device mountpoint or its sub folder, this will auto-mount the device before jump. + # -> Works with any command or any bookmark plugin that change cwd. For example, use `yamb` to add bookmarks and jump to them, use yazi's built-in `cd` `back` `forward` commands: + + # -> { on = [ "m", "a" ], run = [ "plugin yamb -- save", "plugin gvfs -- automount-when-cd" ], desc = "Add bookmark and enable automount when cd"} + { on = [ "M", "t" ], run = "plugin gvfs -- automount-when-cd", desc = "Enable automount when cd to device under cwd" }, + { on = [ "M", "T" ], run = "plugin gvfs -- automount-when-cd --disabled", desc = "Disable automount when cd to device under cwd" }, +] +``` + +It's highly recommended to add these lines to your `~/.config/yazi/yazi.toml`, +because GVFS is slow that can make yazi freeze when it preloads or previews a large number of files. +Especially when you use `Google-drive` or `One-drive`. + +- Replace `1000` with your real user id (run `id -u` to get user id). +- Replace `USER_NAME` with your real user name (run `whoami` to get username). + +> [!IMPORTANT] +> +> For yazi (>=v25.12.29) replace `name` with `url` + +```toml +[plugin] +prepend_preloaders = [ + # Do not preload files in mounted locations: + # Environment variable won't work here. + # Using absolute path instead. + { name = "/run/user/1000/gvfs/**/*", run = "noop" }, + + # For mounted hard disk/drive + { name = "/run/media/USER_NAME/**/*", run = "noop" }, +] +prepend_previewers = [ + # Allow to preview folder. + { name = "*/", run = "folder" }, + + # Do not previewing files in mounted locations. + # Uncomment the line below to allow previewing text files. + # { mime = "{text/*,application/x-subrip}", run = "code" }, + + # Using absolute path. + { name = "/run/user/1000/gvfs/**/*", run = "noop" }, + + # For mounted hard disk/drive. + { name = "/run/media/USER_NAME/**/*", run = "noop" }, +] +``` + +## Note for mounting using fstab + +If you are using fstab to mount, you need to add `x-gvfs-show` to the mount options. +And with it you can only use `jump-to-device` and `jump-back-prev-cwd` actions. + +- Example `/etc/fstab`: + - Mount on demand (manually mount): + + ``` + 192.168.1.10/hdd /mnt/myshare cifs noauto,credentials=/etc/samba/credentials,x-gvfs-show,iocharset=utf8,uid=1000,gid=1000,file_mode=0660,dir_mode=0770,nofail 0 0 + UUID=XXXX-XXXX /mnt/myshare2 exfat noauto,defaults,x-gvfs-show 0 0 + ``` + + - Mount on access, add `x-systemd-automount` to the mount options: + + Use `jump-to-device --automount`. + This will auto mount all mount entries that have `x-systemd-automount`, before jump to. + + - Mount at boot, remove `noauto` from the mount options + + Reload fstab: + + ```sh + sudo systemctl daemon-reload && sudo systemctl restart local-fs.target && sudo mount -a + ``` + + If you changed mount options (like uid=, gid=, umask=, exfat, ntfs, etc.), already-mounted filesystems won't update unless you unmount and remount them. + You can manually remount a specific entry with these commands: + + ```sh + sudo umount /mnt/myshare + sudo mount -a + ``` + +## Troubleshooting + +If you have any problems with one of the protocols, please manually mount it using `gio mount Scheme/Mount URI`. See the [list of supported schemes](). +Then create an issue ticket and include the output of `gio mount -li` along with the list of mount points under `/run/user/1000/gvfs/` and `/run/media/USERNAME` diff --git a/.config/yazi/plugins/gvfs.yazi/assets/automount.sh b/.config/yazi/plugins/gvfs.yazi/assets/automount.sh new file mode 100644 index 0000000..ff0ebb0 --- /dev/null +++ b/.config/yazi/plugins/gvfs.yazi/assets/automount.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# Iterate over all automount units +for unit in $(systemctl list-units --type=automount --no-legend | awk '{print $1}'); do + # Extract the mount path from the unit name + path=$(systemctl show -p Where --value "$unit") + + # Skip certain system directories + case "$path" in + /proc* | /sys* | /dev*) continue ;; + esac + + # Derive the corresponding .mount unit name from the path + mount_unit=$(systemd-escape -p --suffix=mount "$path") + + # Check if the .mount unit is active (i.e., the filesystem is mounted) + if systemctl is-active --quiet "$mount_unit"; then + continue + fi + + # If not mounted, log and then access the directory to trigger the automount + ls "$path" >/dev/null 2>&1 & +done diff --git a/.config/yazi/plugins/gvfs.yazi/assets/gnome-keyring-daemon.service b/.config/yazi/plugins/gvfs.yazi/assets/gnome-keyring-daemon.service new file mode 100644 index 0000000..da29143 --- /dev/null +++ b/.config/yazi/plugins/gvfs.yazi/assets/gnome-keyring-daemon.service @@ -0,0 +1,9 @@ +[Unit] +Description=GNOME Keyring Daemon + +[Service] +ExecStart=/usr/bin/gnome-keyring-daemon --foreground --components=secrets,pkcs11,ssh +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/.config/yazi/plugins/gvfs.yazi/main.lua b/.config/yazi/plugins/gvfs.yazi/main.lua new file mode 100644 index 0000000..16bc6e8 --- /dev/null +++ b/.config/yazi/plugins/gvfs.yazi/main.lua @@ -0,0 +1,2530 @@ +--- @since 25.5.31 + +local M = {} +local SHELL = os.getenv("SHELL") or "" +local HOME = os.getenv("HOME") or "" +local PLUGIN_NAME = "gvfs" + +local USER_ID = ya.uid() +local USER_NAME = tostring(ya.user_name(USER_ID)) +local XDG_RUNTIME_DIR = os.getenv("XDG_RUNTIME_DIR") or ("/run/user/" .. USER_ID) + +local GVFS_ROOT_MOUNTPOINT = XDG_RUNTIME_DIR and (XDG_RUNTIME_DIR .. "/gvfs") or (HOME .. "/.gvfs") +local GVFS_ROOT_MOUNTPOINT_FILE = "/run/media/" .. USER_NAME +local SECRET_TOOL = "secret-tool" +local GPG_TOOL = "gpg" +local PASS_TOOL = "pass" +local SECRET_VAULT_VERSION = "1" +local path_separator = package.config:sub(1, 1) + +---@enum NOTIFY_MSG +local NOTIFY_MSG = { + CANT_CREATE_SAVE_FOLDER = "Can't create save folder: %s", + CANT_SAVE_DEVICES = "Can't write to save file: %s", + CMD_NOT_FOUND = 'Command "%s" not found. Make sure it is installed.', + MOUNT_SUCCESS = 'Mounted: "%s"', + MOUNT_ERROR = "Mount error: %s", + CANT_REMOUNT_DEVICE = "This device can't be remounted or already mounted: %s", + CANT_AUTOMOUNT = "This device can't be automounted: %s", + UNMOUNT_ERROR = "Unmount error: %s", + READING_GVFS_MOUNTED_FOLDER_ERROR = "Reading gvfs mounted folder error: %s", + UNMOUNT_SUCCESS = 'Unmounted: "%s"', + EJECT_SUCCESS = 'Ejected "%s", it can safely be removed', + LIST_DEVICES_EMPTY = "No device or URI found.", + REMOVED_MOUNT_URI = "Device or URI removed: %s", + ADDED_MOUNT_URI = "Device or URI added: %s", + UPDATED_MOUNT_URI = "Device or URI updated: %s", + DEVICE_IS_DISCONNECTED = "Device or URI is disconnected", + CANT_ACCESS_PREV_CWD = "Device or URI is disconnected or Previous directory is removed", + URI_CANT_BE_EMPTY = "URI can't be empty", + URI_IS_INVALID = "URI is invalid", + UNSUPPORTED_SCHEME = "Unsupported scheme %s", + UNSUPPORTED_MANUALLY_MOUNT_SCHEME = "%s scheme is mounted automatically via GNOME Online Accounts (GOA)", + DISPLAY_NAME_CANT_BE_EMPTY = "Display name can't be empty", + MOUNT_ERROR_PASSWORD = 'Failed to mount "%s", please check your password', + MOUNT_ERROR_USERNAME = 'Failed to mount "%s", please check your username', + HEADLESS_DETECTED = "GVFS.yazi plugin can only run on DBUS session. Check github HEADLESS_WORKAROUND.md to enable DBUS session", + LIST_MOUNTS_EMPTY = "List mounts URI is empty", + RETRIVE_PASSWORD_SUCCESS = "Retrieved password from secret vault", + SAVE_PASSWORD_SUCCESS = "Saved password to secret vault", + SAVE_PASSWORD_FAILED = "Save password failed: %s", + SECRET_VAULT_LOCKED = "Secret vault is locked%s", + PASS_INIT_GPG_ID = 'Please run "pass init " to initialize your GPG key first. \nCheck SECURE_SAVED_PASSWORD.md for the fix', + MISSING_PUBLIC_KEY_GPG_KEY = "GPG key is missing public key\nCheck SECURE_SAVED_PASSWORD.md for the fix", + AUTOMOUNT_WHEN_CD_STATE = "%s automount when cd for: %s", +} + +---@enum PASSWORD_VAULT +local PASSWORD_VAULT = { + KEYRING = "keyring", + PASS = "pass", +} + +---@enum DEVICE_CONNECT_STATUS +local DEVICE_CONNECT_STATUS = { + MOUNTED = 1, + NOT_MOUNTED = 2, +} + +---@enum SCHEME +local SCHEME = { + MTP = "mtp", + SMB = "smb", + SFTP = "sftp", + SSH = "ssh", -- Alias for sftp + NFS = "nfs", + GPHOTO2 = "gphoto2", + FTP = "ftp", + FTPS = "ftps", + FTPIS = "ftpis", + GOOGLE_DRIVE = "google-drive", + ONE_DRIVE = "onedrive", + DNS_SD = "dns-sd", + DAV = "dav", + DAVS = "davs", + DAVSD = "dav+sd", + DAVSSD = "davs+sd", + AFP = "afp", + AFC = "afc", + FILE = "file", +} + +---@enum STATE_KEY +local STATE_KEY = { + PREV_CWD = "PREV_CWD", + WHICH_KEYS = "WHICH_KEYS", + CMD_FOUND = "CMD_FOUND", + DBUS_SESSION = "DBUS_SESSION", + ROOT_MOUNTPOINT = "ROOT_MOUNTPOINT", + SAVE_PATH = "SAVE_PATH", + SAVE_PATH_AUTOMOUNTS = "SAVE_PATH_AUTOMOUNTS", + MOUNTS = "MOUNTS", + AUTOMOUNTS = "AUTOMOUNTS", + SAVE_PASSWORD_AUTOCONFIRM = "SAVE_PASSWORD_AUTOCONFIRM", + PASSWORD_VAULT = "PASSWORD_VAULT", + KEY_GRIP = "KEY_GRIP", + INPUT_POSITION = "INPUT_POSITION", + TASKS_LOAD_GDRIVE_FOLDER = "TASKS_LOAD_GDRIVE_FOLDER", + TASKS_LOAD_GDRIVE_FOLDER_RUNNING = "TASKS_LOAD_GDRIVE_FOLDER_RUNNING", + BLACKLIST_DEVICES = "BLACKLIST_DEVICES", + CACHED_LOCAL_PATH_DEVICE = "CACHED_LOCAL_PATH_DEVICE", +} + +---@enum ACTION +local ACTION = { + SELECT_THEN_MOUNT = "select-then-mount", + JUMP_TO_DEVICE = "jump-to-device", + JUMP_BACK_PREV_CWD = "jump-back-prev-cwd", + SELECT_THEN_UNMOUNT = "select-then-unmount", + REMOUNT_KEEP_CWD_UNCHANGED = "remount-current-cwd-device", + ADD_MOUNT = "add-mount", + EDIT_MOUNT = "edit-mount", + REMOVE_MOUNT = "remove-mount", + LOAD_GDRIVE_FOLDER = "load-gdrive-folder", + CACHE_LOCAL_PATH_DEVICE = "cache-local-path-device", + AUTOMOUNT_WHEN_CD = "automount-when-cd", + MOUNT_THEN_JUMP_SUBFOLDER = "mount-then-jump-subfolder", +} + +---@class (exact) GdriveMountedFolderAttribute +---@field display_name string +---@field is_symlink "FALSE" | "TRUE" +---@field name string +---@field type string +---@field size number +---@field modified number +---@field created number +---@field access number +---@field can_read boolean +---@field can_write boolean +---@field can_execute boolean +---@field can_delete boolean +---@field can_trash boolean +---@field can_rename boolean + +---@class (exact) ChildrenFolderGioInfo +---@field display_name string +---@field uri string +---@field unix_mount string +---@field name string +---@field type string +---@field local_path string +---@field attributes GdriveMountedFolderAttribute + +---@class (exact) Device +---@field name string +---@field class string? +---@field mounts Mount[] +---@field scheme SCHEME +---@field bus integer? +---@field device integer? +---@field uuid string? +---@field encrypted_uuid string? +---@field service_domain string? +---@field ["unix-device"] string? +---@field owner string? +---@field activation_root string? +---@field uri string +---@field is_manually_added boolean? +---@field can_mount "1"|"0"|nil +---@field can_unmount "1"|"0" +---@field can_eject "1"|"0" +---@field should_automount "1"|"0" +---@field remote_path string? + +---@class (exact) Mount +---@field name string +---@field class string? +---@field uri string +---@field scheme SCHEME +---@field bus integer? +---@field device integer? +---@field uuid string? +---@field ["unix-device"] string? +---@field owner string? +---@field default_location string? +---@field can_unmount "1"|"0"|nil +---@field can_eject "1"|"0"|nil +---@field is_shadowed "1"|"0"|nil + +-- Encode binary string to hex (e.g., "\xED" => "\\xED") +local function hex_encode(s) + return (s:gsub(".", function(c) + return string.format("\\x%02X", c:byte()) + end)) +end + +-- Decode hex-encoded string (e.g., "\\xED" => "\xED") +local function hex_decode(s) + return (s:gsub("\\x(%x%x)", function(hex) + return string.char(tonumber(hex, 16)) + end)) +end + +local function hex_encode_table(t) + local out = {} + for k, v in pairs(t) do + local new_k = type(k) == "string" and hex_encode(k) or k + local new_v + if type(v) == "table" then + new_v = hex_encode_table(v) + elseif type(v) == "string" then + new_v = hex_encode(v) + else + new_v = v + end + out[new_k] = new_v + end + return out +end + +local function hex_decode_table(t) + local out = {} + for k, v in pairs(t) do + local new_k = type(k) == "string" and hex_decode(k) or k + local new_v + if type(v) == "table" then + new_v = hex_decode_table(v) + elseif type(v) == "string" then + new_v = hex_decode(v) + else + new_v = v + end + out[new_k] = new_v + end + return out +end + +local set_state_table = ya.sync(function(state, table, key, value) + if type(table) == "string" and type(key) == "string" then + if not state[table] then + state[table] = {} + end + state[table][key] = value + end +end) +local set_state = ya.sync(function(state, key, value) + state[key] = value +end) + +local get_state = ya.sync(function(state, key) + return state[key] +end) + +local enqueue_task = ya.sync(function(state, task_name, task_data) + if not state[task_name] or type(state[task_name]) ~= "table" then + state[task_name] = {} + end + for _, _task_data in ipairs(state[task_name]) do + if _task_data.folder_to_load == task_data.folder_to_load then + return + end + end + table.insert(state[task_name], task_data) +end) + +local dequeue_task = ya.sync(function(state, task_name) + if not state[task_name] or type(state[task_name]) ~= "table" then + return {} + end + return table.remove(state[task_name]) +end) + +---@param is_password boolean? +local function show_input(title, is_password, value) + local input_value, input_pw_event = ya.input({ + title = title, + value = value or "", + obscure = is_password or false, + pos = get_state(STATE_KEY.INPUT_POSITION), + -- TODO: remove this after next yazi released + position = get_state(STATE_KEY.INPUT_POSITION), + }) + if input_pw_event ~= 1 then + return nil, nil + end + return input_value, input_pw_event +end + +local function error(s, ...) + ya.notify({ title = PLUGIN_NAME, content = string.format(s, ...), timeout = 3, level = "error" }) +end + +local function info(s, ...) + ya.notify({ title = PLUGIN_NAME, content = string.format(s, ...), timeout = 3, level = "info" }) +end + +---run any command +---@param cmd string +---@param args string[] +---@param _stdin? Stdio|nil +---@return Error|nil, Output|nil +local function run_command(cmd, args, _stdin) + local stdin = _stdin or Command.INHERIT + local child, cmd_err = Command(cmd) + :arg(args) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :stdin(stdin) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :spawn() + + if not child then + error("Failed to start `%s` with error: `%s`", cmd, cmd_err) + return cmd_err, nil + end + + local output, out_err = child:wait_with_output() + if not output then + error("Cannot read `%s` output, error: `%s`", cmd, out_err) + return out_err, nil + else + return nil, output + end +end + +local function is_in_dbus_session() + local dbus_session = get_state(STATE_KEY.DBUS_SESSION) + if dbus_session == nil then + local cha, _ = fs.cha(Url(XDG_RUNTIME_DIR)) + dbus_session = cha and true or false + set_state(STATE_KEY.DBUS_SESSION, dbus_session) + end + return dbus_session +end + +local function is_cmd_exist(cmd) + local cmd_found = get_state(STATE_KEY.CMD_FOUND .. cmd) + if cmd_found == nil then + local _, output = run_command("which", { cmd }) + cmd_found = output and output.status and output.status.success + set_state(STATE_KEY.CMD_FOUND .. cmd, cmd_found) + end + return cmd_found +end + +local function pathJoin(...) + -- Detect OS path separator ('\' for Windows, '/' for Unix) + local separator = package.config:sub(1, 1) + local parts = { ... } + local filteredParts = {} + -- Remove empty strings or nil values + for _, part in ipairs(parts) do + if part and part ~= "" then + table.insert(filteredParts, part) + end + end + -- Join the remaining parts with the separator + local path = table.concat(filteredParts, separator) + -- Normalize any double separators (e.g., "folder//file" → "folder/file") + path = path:gsub(separator .. "+", separator) + + return path +end + +local function is_folder_exist(path) + local err, output = run_command("[", { "-d", path, "]" }) + return output and output.status and output.status.success +end + +local function tbl_remove_empty(tbl) + local cleaned = {} + for _, v in pairs(tbl) do + if v ~= nil and v ~= "" then + table.insert(cleaned, v) + end + end + return cleaned +end + +local current_dir = ya.sync(function() + return tostring(cx.active.current.cwd.path or cx.active.current.cwd) +end) + +---@enum PUBSUB_KIND +local PUBSUB_KIND = { + cd = "cd", + hover = "hover", + mounts_changed = PLUGIN_NAME .. "-" .. "mounts-changed", + automounts_changed = PLUGIN_NAME .. "-" .. "automounts-changed", + unmounted = PLUGIN_NAME .. "-" .. "unmounted", +} + +--- broadcast through pub sub to other instances +---@param _ table state +---@param pubsub_kind PUBSUB_KIND +---@param data any +---@param to number default = 0 to all instances +local broadcast = ya.sync(function(_, pubsub_kind, data, to) + ps.pub_to(to or 0, pubsub_kind, data) +end) + +local is_dir = function(dir_path) + local cha, err = fs.cha(Url(dir_path)) + return not err and cha and cha.is_dir +end + +---split string by char +---@param s string +---@return string[] +local function string_to_array(s) + local array = {} + for i = 1, #s do + table.insert(array, s:sub(i, i)) + end + return array +end + +local function is_literal_string(str) + return str and str:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") +end + +local function tbl_deep_clone(original) + if type(original) ~= "table" then + return original + end + + local copy = {} + for key, value in pairs(original) do + copy[tbl_deep_clone(key)] = tbl_deep_clone(value) + end + + return copy +end + +local function path_quote(path) + if not path or path == "" then + return path + end + local result = "'" .. string.gsub(tostring(path), "'", "'\\''") .. "'" + return result +end + +local get_hovered_path = ya.sync(function() + local h = cx.active.current.hovered.path or cx.active.current.hovered + if h then + return tostring(h.url) + end +end) + +local function is_secret_vault_available_keyring(unlock_vault_dialog) + local res, err = Command(SECRET_TOOL) + :arg({ + "search", + PLUGIN_NAME, + SECRET_VAULT_VERSION, + unlock_vault_dialog and "--unlock" or nil, + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + + if err or (res and res.stderr and res.stderr:match("^secret%-tool")) then + return false + end + return true +end + +local function build_secret_vault_entry_gpg(protocol, user, domain, prefix, port, service_domain) + protocol = protocol and ("/" .. protocol) or "" + user = user and ("/" .. user) or "" + domain = domain and ("/" .. domain) or "" + prefix = prefix and ("/" .. prefix) or "" + port = port and ("/" .. port) or "" + service_domain = service_domain and ("/" .. service_domain) or "" + return PLUGIN_NAME .. "/" .. SECRET_VAULT_VERSION .. protocol .. user .. domain .. port .. prefix .. service_domain +end + +local function is_secret_vault_available_gpg(unlock_vault_dialog, is_second_run) + local test_vault_entry = build_secret_vault_entry_gpg("test") + local res, err = Command(SHELL) + :arg({ + "-c", + "gpg-connect-agent 'keyinfo " .. get_state(STATE_KEY.KEY_GRIP) .. "' /bye", + }) + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + + if res then + -- Case unlocked + if res.stdout:match(".* KEYINFO [^ ]+ .+ .+ .+ 1 ") or res.stdout:match(".* KEYINFO [^ ]+ .+ .+ .+ .+ C ") then + return true + elseif unlock_vault_dialog and res.stdout:match(".* KEYINFO [^ ]+ .+ .+ .+ - ") then + -- Display gpg unlock TUI window + -- TODO: remove this after next yazi released + local permit = (ui.hide or ya.hide)() + -- Wrap in shell to capture exit code + local full_cmd = string.format("bash -c '%s; echo __EXIT__$?__'", "pass " .. test_vault_entry .. " 2>&1") + local handle = io.popen(full_cmd) + local output = handle:read("*a") + handle:close() + + -- Extract exit code + local exit_code = tonumber(output:match("__EXIT__(%d+)__")) + output = output:gsub("__EXIT__%d+__", ""):gsub("%s+$", "") -- clean output + permit:drop() + if output:match("Error: .* is not in the password store") then + res, err = Command(SHELL) + :arg({ + "-c", + ("printf '%s\n%s\n' " .. path_quote("test") .. " " .. path_quote("test") .. " | ") + .. PASS_TOOL + .. " insert " + .. " -f " + .. path_quote(test_vault_entry), + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + if res and res.stderr:match("Error: You must run:") and res.stderr:match("pass init your%-gpg%-id") then + error(NOTIFY_MSG.PASS_INIT_GPG_ID) + return false + end + if + res + and res.stderr:match("encryption failed: No public key") + and res.stderr:match("Password encryption aborted") + then + error(NOTIFY_MSG.MISSING_PUBLIC_KEY_GPG_KEY) + return false + end + + if is_second_run or err or (res and res.status and not res.status.success) then + return false + end + return is_secret_vault_available_gpg(unlock_vault_dialog, true) + end + return exit_code == 0 + end + end + return false +end + +local function is_secret_vault_available(unlock_vault_dialog) + if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.KEYRING then + return is_secret_vault_available_keyring(unlock_vault_dialog) + elseif get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.PASS then + return is_secret_vault_available_gpg(unlock_vault_dialog) + end + return nil +end + +local function save_password_keyring(password, protocol, user, domain, prefix, port, service_domain) + if not user or not password or not protocol or not domain then + return false + end + + local res, err = Command(SHELL) + :arg({ + "-c", + ("printf %s " .. path_quote(password) .. " | ") + .. SECRET_TOOL + .. " store " + .. " --label " + .. path_quote( + protocol + .. "://" + .. user + .. "@" + .. domain + .. (port and (":" .. port) or "") + .. (prefix and ("/" .. prefix) or "") + .. (service_domain and ("/" .. service_domain) or "") + ) + .. " " + .. PLUGIN_NAME + .. " " + .. SECRET_VAULT_VERSION + .. " protocol " + .. protocol + .. " user " + .. path_quote(user) + .. " domain " + .. path_quote(domain) + .. (port and (" port " .. port) or "") + .. (prefix and (" prefix " .. path_quote(prefix)) or "") + .. (service_domain and (" service_domain " .. path_quote(service_domain)) or ""), + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + if res and res.stderr then + if res.stderr:match("secret%-tool: Cannot get secret of a locked object") then + error(NOTIFY_MSG.SECRET_VAULT_LOCKED) + return false + elseif res.stderr:match("secret%-tool: The name is not activatable") then + error(NOTIFY_MSG.HEADLESS_DETECTED) + return false + elseif res.stderr:match("secret%-tool: Cannot autolaunch D%-Bus") then + error(NOTIFY_MSG.HEADLESS_DETECTED) + return false + end + end + if err or (res and not res.status.success and res.stderr) then + error(NOTIFY_MSG.SAVE_PASSWORD_FAILED, res and res.stderr or err) + return false + end + info(NOTIFY_MSG.SAVE_PASSWORD_SUCCESS) + return true +end + +local function save_password_gpg(password, protocol, user, domain, prefix, port, service_domain) + if not user or not password or not protocol or not domain then + return false + end + + local res, err = Command(SHELL) + :arg({ + "-c", + ("printf '%s\n%s\n' " .. path_quote(password) .. " " .. path_quote(password) .. " | ") + .. PASS_TOOL + .. " insert " + .. " -f " + .. path_quote(build_secret_vault_entry_gpg(protocol, user, domain, prefix, port, service_domain)), + }) + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + + if err or (res and res.status and not res.status.success) then + error(NOTIFY_MSG.SAVE_PASSWORD_FAILED, res and res.stderr or err) + return false + end + + info(NOTIFY_MSG.SAVE_PASSWORD_SUCCESS) + return true +end + +local function save_password(password, protocol, user, domain, prefix, port, service_domain) + if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.KEYRING then + return save_password_keyring(password, protocol, user, domain, prefix, port, service_domain) + elseif get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.PASS then + return save_password_gpg(password, protocol, user, domain, prefix, port, service_domain) + end + return false +end + +local function lookup_password_keyring(protocol, user, domain, prefix, port, service_domain) + if not user or not protocol or not domain then + return nil + end + local res, err = Command(SECRET_TOOL) + :arg(tbl_remove_empty({ + "lookup", + PLUGIN_NAME, + SECRET_VAULT_VERSION, + "protocol", + protocol, + "user", + user, + "domain", + domain, + port and "port" or nil, + port and port or nil, + prefix and "prefix" or nil, + prefix and prefix or nil, + service_domain and "service_domain" or nil, + service_domain and service_domain or nil, + })) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + if not err and res and res.status and res.status.success then + return res.stdout + end + + return nil +end + +local function lookup_password_gpg(protocol, user, domain, prefix, port, service_domain) + if not user or not protocol or not domain then + return nil + end + local res, err = Command(PASS_TOOL) + :arg({ + build_secret_vault_entry_gpg(protocol, user, domain, prefix, port, service_domain), + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + if not err and res and res.status and res.status.success then + return res.stdout + end + + return nil +end + +local function lookup_password(protocol, user, domain, prefix, port, service_domain) + if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.KEYRING then + return lookup_password_keyring(protocol, user, domain, prefix, port, service_domain) + elseif get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.PASS then + return lookup_password_gpg(protocol, user, domain, prefix, port, service_domain) + end + return nil +end + +local function clear_password_keyring(protocol, user, domain, prefix, port, service_domain) + local res, err = Command(SECRET_TOOL) + :arg(tbl_remove_empty({ + "clear", + PLUGIN_NAME, + SECRET_VAULT_VERSION, + protocol and "protocol" or nil, + protocol and protocol or nil, + user and "user" or nil, + user and user or nil, + domain and "domain" or nil, + domain and domain or nil, + port and "port" or nil, + port and port or nil, + prefix and "prefix" or nil, + prefix and prefix or nil, + service_domain and "service_domain" or nil, + service_domain and service_domain or nil, + })) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + if res and res.stderr and not res.status.success then + if res.stderr:match("secret%-tool: Cannot get secret of a locked object") then + error(NOTIFY_MSG.SECRET_VAULT_LOCKED) + return false + elseif res.stderr:match("secret%-tool: The name is not activatable") then + error(NOTIFY_MSG.HEADLESS_DETECTED) + return false + elseif res.stderr:match("secret%-tool: Cannot autolaunch D%-Bus") then + error(NOTIFY_MSG.HEADLESS_DETECTED) + return false + end + end + + if not err and res and res.status and res.status.success then + return true + end + return false +end + +local function clear_password_gpg(protocol, user, domain, prefix, port, service_domain) + local res, err = Command(PASS_TOOL) + :arg({ + "rm", + "-r", + "-f", + build_secret_vault_entry_gpg(protocol, user, domain, prefix, port, service_domain), + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + if not err and res and res.status and res.status.success then + return true + end + return false +end + +local function clear_password(protocol, user, domain, prefix, port, service_domain) + if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.KEYRING then + return clear_password_keyring(protocol, user, domain, prefix, port, service_domain) + elseif get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.PASS then + return clear_password_gpg(protocol, user, domain, prefix, port, service_domain) + end + return false +end + +local function extract_info_from_uri(s) + local user + local domain + local port + local service_domain + + -- Attempt 1: Look for user@domain:port first (if it exists) + local scheme, temp_user, temp_domain_part = s:match("^([^:]+)://([^@/]+)@([^/]+)") + if temp_user and temp_domain_part then + -- If user@domain found, the domain might be followed by a comma + -- We want the part before the first comma or slash in the domain part + user = temp_user + -- domain:port + domain, port = temp_domain_part:match("^([^:/]+):([^:/]+)") + if not port or port == "" then + port = nil + domain = temp_domain_part:match("^[^/]+") or temp_domain_part + end + else + -- Attempt 2: No user@domain, so try to get domain from the start (before first comma or slash) + scheme, temp_domain_part = s:match("^([^:]+)://([^/]+)") + if temp_domain_part then + domain, port = temp_domain_part:match("^([^:/]+):([^:/]+)") + if not port or port == "" then + port = nil + domain = temp_domain_part:match("^[^/]+") or temp_domain_part + end + end + end + + local ssl = (s:match("^davs") or s:match("^ftps") or s:match("^ftpis") or s:match("^https")) and true or false + local prefix = s:match(".*" .. (is_literal_string(domain) or "") .. (port and ":" .. port or "") .. "/(.+)$") or nil + if user then + local _service_domain, _user = user:match("^([^;]+);(.+)") + user = _service_domain and _user or user + service_domain = _service_domain and _service_domain + end + return scheme, domain, user, ssl, prefix, port, service_domain +end + +local function is_mountpoint_belong_to_volume(mount, volume) + return mount.is_shadowed ~= "1" + and mount.scheme + and mount.scheme == volume.scheme + and ( + (mount.uri and mount.uri == volume.uri) + or (mount.uuid and mount.uuid == volume.uuid) + or (mount["unix-device"] and mount["unix-device"] == volume["unix-device"]) + or (mount.bus and mount.device and mount.bus == volume.bus and mount.device == volume.device) + -- Case fstab with `x-gvfs-show` + or (mount.name and mount.name == volume.name and mount.scheme == SCHEME.FILE) + ) +end + +--- Parser for gvfs mounted gdrive folder info +---@param data string +---@return ChildrenFolderGioInfo[] +local function parse_gdrive_mountpoint_info(data) + ---@type table[] + local result = {} + ---@type table? + local current_item = nil + + -- Iterate over each line of the input data. + for line in data:gmatch("([^\r\n]+)") do + -- First, try to match an indented attribute line, e.g., " standard::type: 2" + local indent, attr_key, attr_value = line:match("^(%s+)[^:]+::([^:]+):%s+(.+)$") + + if indent and attr_key and attr_value then + -- This is an attribute line. Add it to the current item's attribute sub-table. + if current_item then + -- Ensure the 'attributes' sub-table exists. + current_item.attributes = current_item.attributes or {} + + -- Clean the key (e.g., "standard::type" -> "standard_type") and assign the value. + local clean_attr_key = attr_key:match("^%s*(.-)%s*$"):gsub("[%s%-]", "_") + if clean_attr_key == "can_read" then + current_item.attributes[clean_attr_key] = attr_value == "TRUE" + elseif clean_attr_key == "can_write" then + current_item.attributes[clean_attr_key] = attr_value == "TRUE" + elseif clean_attr_key == "can_execute" then + current_item.attributes[clean_attr_key] = attr_value == "TRUE" + elseif clean_attr_key == "can_delete" then + current_item.attributes[clean_attr_key] = attr_value == "TRUE" + elseif clean_attr_key == "can_trash" then + current_item.attributes[clean_attr_key] = attr_value == "TRUE" + elseif clean_attr_key == "can_rename" then + current_item.attributes[clean_attr_key] = attr_value == "TRUE" + else + current_item.attributes[clean_attr_key] = attr_value + end + end + else + -- If it's not an attribute, try to match a top-level key-value pair. + -- The value part `(.*)` allows for keys with no value, like "attributes:". + local key, value = line:match("^%s*([^:]+):%s*(.*)$") + + if key then + -- Clean the key by trimming whitespace and replacing spaces with underscores. + local clean_key = key:match("^%s*(.-)%s*$"):gsub("%s", "_") + + -- The "display_name" key marks the beginning of a new item block. + if clean_key == "display_name" then + -- If we were processing a previous item, add it to the result list. + if current_item then + table.insert(result, current_item) + end + -- Start a new table for the new item. + current_item = {} + end + + -- Add the property to the current item, provided we are inside an item block + -- and the key has a value. + if current_item and value and value ~= "" then + current_item[clean_key] = value + end + end + end + end + + -- After the loop, if there's a pending item, add it to the results. + if current_item then + table.insert(result, current_item) + end + + return result +end + +local function get_gdrive_children_folder_info(parent_folder) + if not parent_folder then + return + end + parent_folder = tostring(parent_folder) + if + parent_folder:match( + "^" .. is_literal_string(get_state(STATE_KEY.ROOT_MOUNTPOINT) .. "/google-drive:host=gmail.com") + ) + then + local output, err = Command(SHELL) + :arg({ + "-c", + "gio info -a standard::display-name,standard::name,time::modified,time::created,time::access,standard::type,standard::is-symlink,standard::size,access::can-read,access::can-write,access::can-execute,access::can-delete,access::can-delete,access::can-rename " + .. path_quote(parent_folder) + .. "/*", + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :env("LC_ALL", "C") + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + if err then + error(NOTIFY_MSG.READING_GVFS_MOUNTED_FOLDER_ERROR, tostring(err)) + return nil + end + if output and output.status.code == 0 then + return parse_gdrive_mountpoint_info(output.stdout) + end + end +end + +--- Display real folder name using virtual files/folders +---@param children_folder_info ChildrenFolderGioInfo[] +local function display_virtual_children(cwd, children_folder_info) + if children_folder_info == nil or #children_folder_info == 0 then + return + end + local id = ya.id("ft") + local files = {} + + ya.emit("update_files", { op = fs.op("part", { id = id, url = Url(cwd), files = {} }) }) + + for _, gdrive_mountpoint_info in ipairs(children_folder_info) do + if id ~= nil and id ~= "" then + -- TODO: WORKAROUND: cwd prefix `search://` can't be joined + -- local url = Url(cwd):join(gdrive_mountpoint_info.display_name) + local url = + Url(tostring(cwd.path or cwd) .. path_separator .. tostring(gdrive_mountpoint_info.display_name)) + + local kind = gdrive_mountpoint_info.type == "directory" and 1 + or ( + gdrive_mountpoint_info.type == "regular" and 0 + or (gdrive_mountpoint_info.type == "shortcut" and 4 or 16) + ) + + -- Fix for Google AI Studio using max unsigned 64-bit integer value + if gdrive_mountpoint_info.attributes.access == "18446744073709551615" then + gdrive_mountpoint_info.attributes.access = nil + end + if gdrive_mountpoint_info.attributes.modified == "18446744073709551615" then + gdrive_mountpoint_info.attributes.modified = nil + end + if gdrive_mountpoint_info.attributes.created == "18446744073709551615" then + gdrive_mountpoint_info.attributes.created = nil + end + table.insert( + files, + File({ + url = url, + cha = Cha({ + kind = kind, + mode = tonumber(kind == 1 and "40700" or "100644", 8), + len = tonumber(gdrive_mountpoint_info.attributes.size or "0"), + atime = gdrive_mountpoint_info.attributes.access, + mtime = gdrive_mountpoint_info.attributes.modified, + ctime = gdrive_mountpoint_info.attributes.created, + btime = gdrive_mountpoint_info.attributes.modified, + }), + }) + ) + end + end + + ya.emit("update_files", { op = fs.op("part", { id = id, url = Url(cwd), files = files }) }) + ya.emit("update_files", { op = fs.op("done", { id = id, url = Url(cwd), cha = fs.cha(Url(cwd), false) }) }) +end + +local function parse_devices(raw_input) + local volumes = {} + local mounts = {} + local predefined_mounts = tbl_deep_clone(get_state(STATE_KEY.MOUNTS)) or {} + local blacklist_devices = get_state(STATE_KEY.BLACKLIST_DEVICES) or {} + ---@type Device? + local current_volume = nil + ---@type Mount? + local current_mount = nil + + for m = #predefined_mounts, 1, -1 do + local pm = predefined_mounts[m] + if pm.uri then + -- replace ssh:// with sftp:// + if pm.scheme == SCHEME.SSH then + pm.scheme = SCHEME.SFTP + pm.uri = pm.uri:gsub("^ssh://", "sftp://") + end + -- keep remote path, for jumping. Only scheme that doesn't support remote path + if + pm.scheme == SCHEME.SFTP + or pm.scheme == SCHEME.FTP + or pm.scheme == SCHEME.FTPS + or pm.scheme == SCHEME.FTPIS + or pm.scheme == SCHEME.DNS_SD + or pm.scheme == SCHEME.AFC + then + pm.remote_path = pm.uri:match("^[%w+]+://[^/]+/(.+)$") or "" + -- Remove remote path + pm.uri = pm.uri:gsub("^(%a+://[^/]+).*", "%1") + end + end + end + + for line in raw_input:gmatch("[^\r\n]+") do + local clean_line = line:match("^%s*(.-)%s*$") + + -- Match volume(0) + local volume_name = clean_line:match("^Volume%(%d+%):%s*(.+)$") + if line:match("^Drive%(%d+%):") then + current_mount = nil + current_volume = nil + elseif volume_name then + current_mount = nil + current_volume = { name = volume_name, mounts = {} } + table.insert(volumes, current_volume) + + -- Match mount(0) + elseif clean_line:match("^Mount%(%d+%):") then + current_mount = nil + local mount_indent, mount_name, mount_uri = line:match("^(%s*)Mount%(%d+%):%s*(.-)%s*->%s*(.+)$") + if not mount_name then + mount_name = clean_line:match("^Mount%(%d+%):%s*(.+)$") + end + + if not mount_indent or #mount_indent == 0 then + current_volume = nil + end + current_mount = { name = mount_name or "", uri = mount_uri or "" } + + local mount_base_uri = mount_uri:gsub("/+$", "") + local mount_uri_port = mount_base_uri:match(":%d+$") + for m = #predefined_mounts, 1, -1 do + local predefined_mount_base_uri = predefined_mounts[m].uri:gsub("/+$", "") + if + predefined_mount_base_uri == mount_base_uri + or ( + not mount_uri_port + and predefined_mount_base_uri:gsub(":%d+$", "") == mount_base_uri:gsub(":%d+$", "") + ) + then + current_mount = table.remove(predefined_mounts, m) + end + end + + if not current_mount.scheme then + for _, value in pairs(SCHEME) do + if mount_uri:match("^" .. is_literal_string(value) .. ":") then + current_mount.scheme = value + end + end + end + + -- Case mtp/gphoto2 usb bus dev + if mount_uri then + local protocol, bus, device = mount_uri:match("^(%w+)://%[usb:(%d+),(%d+)%]/") + -- Attach to mount or volume + if protocol and (protocol == SCHEME.MTP or protocol == SCHEME.GPHOTO2) and bus and device then + current_mount.bus = bus + current_mount.device = device + end + -- file:///run/media/huyhoang/6412-E4B2 + local owner, label_or_uuid = mount_uri:match("^file:///run/media/(.+)/(.+)") + if owner and label_or_uuid then + current_mount.owner = owner + current_mount.uuid = current_volume and (current_volume.uuid or current_volume["unix-device"]) + or label_or_uuid + current_mount["unix-device"] = current_volume and current_volume["unix-device"] + end + end + table.insert(mounts, current_mount) + + -- Match key=value metadata + else + local key, value = clean_line:match("^(%S+)%s*=%s*(.+)$") + if not key or not value then + key, value = clean_line:match("^(%S+)%s*:%s*'(.-)'$") + if key == "uuid" and value then + current_volume.encrypted_uuid = value + end + end + if key and value then + -- Attach to mount or volume + local target = current_mount or current_volume + if target then + if key ~= "name" or not target[key] then + target[key] = value + end + end + else + local bus, device = line:match(".*:%s*'/dev/bus/usb/(%d+)/(%d+)'") + -- Attach to mount or volume + if bus and device then + local target = current_mount or current_volume + if target then + target.bus = bus + target.device = device + end + end + end + end + end + + -- Remove shadowed mounts and attach mount points to volumes + for i = #volumes, 1, -1 do + local v = volumes[i] + if v.activation_root then + v.uri = v.activation_root + end + + if not v.uuid and v.class == "device" and v["unix-device"] then + v.uuid = v["unix-device"] + v.scheme = SCHEME.FILE + -- Attach scheme to volume + elseif (v.can_mount == "0") or v.uuid and not v.uuid:match("([^:]+)://(.+)") then + -- NOTE: can_mount == "0" means that the volume is mounted fstab + v.scheme = SCHEME.FILE + else + for _, value in pairs(SCHEME) do + if + (v.uri and v.uri:match("^" .. is_literal_string(value) .. ":")) + or (v.uuid and v.uuid:match("^" .. is_literal_string(value) .. "://")) + then + v.scheme = value + end + end + end + -- NOTE: Remove volumes without scheme (fstab) + if volumes[i] and not v.scheme then + table.remove(volumes, i) + end + + -- Attach mount points to volume, then remove it from mounts array + for j = #mounts, 1, -1 do + if is_mountpoint_belong_to_volume(mounts[j], v) then + table.insert(v.mounts, table.remove(mounts, j)) + end + end + end + + -- Remove shadowed mounts and attach unmapped mounts to itself + for _, m in ipairs(mounts) do + if m.is_shadowed ~= "1" and m.uri then + m.mounts = { tbl_deep_clone(m) } + table.insert(volumes, m) + end + end + + for _, m in ipairs(predefined_mounts) do + m.mounts = { tbl_deep_clone(m) } + table.insert(volumes, m) + end + + if #blacklist_devices > 0 then + for i = #volumes, 1, -1 do + local v = volumes[i] + for _, bl_device in pairs(blacklist_devices) do + if type(bl_device) == "string" and v.name == bl_device then + table.remove(volumes, i) + elseif type(bl_device) == "table" then + for bl_device_prop, bl_device_value in pairs(bl_device) do + if v[bl_device_prop] ~= bl_device_value then + goto skip_bl_device + end + end + table.remove(volumes, i) + end + ::skip_bl_device:: + end + end + end + return volumes +end + +---@param device Device +---@return string|nil +local function get_mounted_path(device) + if not device then + return nil + end + if device.uri or (#device.mounts > 0 and device.mounts[1].uri) then + local res, err = Command(SHELL) + :arg({ + "-c", + "gio info " + .. path_quote(device.uri or (#device.mounts > 0 and device.mounts[1].uri)) + .. ' | grep -E "^local path: "', + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :env("LC_ALL", "C") + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + if err or (res and res.status and not res.status.success) then + return nil + end + return res.stdout:match("^local path: (.+)$"):gsub("\n", "") or nil + end + return nil +end + +local function can_device_umount(device) + if device and device.mounts and #device.mounts > 0 then + for _, mount in ipairs(device.mounts) do + if mount.can_unmount == "1" or mount.can_eject == "1" then + return true + end + end + end +end + +---@param device Device +local function is_mounted(device) + if can_device_umount(device) then + return true + end + local mountpath = get_mounted_path(device) + return mountpath and is_folder_exist(mountpath) +end + +---mount device +---@param opts {device: Device, username?:string, password?: string, service_domain?: string, is_pw_saved?: boolean, skipped_secret_vault?: boolean,max_retry?: integer, retries?: integer, anonymous?: boolean} +---@return boolean +local function mount_device(opts) + local device = opts.device + local max_retry = opts.max_retry or 3 + local retries = opts.retries or 0 + local password = opts.password + local is_pw_saved = opts.is_pw_saved + local skipped_secret_vault = opts.skipped_secret_vault + local username = opts.username + local anonymous = opts.anonymous + local service_domain = opts.service_domain + local error_msg = nil + + local auths = "" + local auth_string_format = "" + if password or username then + if username then + auths = path_quote(username) + auth_string_format = auth_string_format .. "%s\n" + end + if service_domain then + auths = auths .. " " .. path_quote(service_domain) + auth_string_format = auth_string_format .. "%s\n" + end + if password then + auths = auths .. " " .. path_quote(password) + auth_string_format = auth_string_format .. "%s\n" + end + end + + local res, err = Command(SHELL) + :arg({ + "-c", + (auth_string_format ~= "" and "printf " .. path_quote(auth_string_format) .. " " .. auths .. " | " or "") + .. " gio mount " + .. (anonymous and "-a " or "") + .. (device.uuid and ("-d " .. device.uuid) or path_quote(device.uri)), + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :env("LC_ALL", "C") + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + + local mount_success = res and res.status and res.status.success + + if mount_success then + info(NOTIFY_MSG.MOUNT_SUCCESS, device.name) + if password and not is_pw_saved and not skipped_secret_vault and is_secret_vault_available() then + local confirmed_save_password = get_state(STATE_KEY.SAVE_PASSWORD_AUTOCONFIRM) + or ya.confirm({ + title = ui.Line("Remember password?"):style(th.confirm.title), + body = ui.Text({ + ui.Line(""), + ui.Line("Press Yes to save password to secret vault."):style(th.confirm.content), + ui.Line(""), + }) + :align(ui.Align.CENTER) + :wrap(ui.Wrap.YES), + -- TODO: remove this after next yazi released + content = ui.Text({ + ui.Line(""), + ui.Line("Press Yes to save password to secret vault."):style(th.confirm.content), + ui.Line(""), + }) + :align(ui.Align.CENTER) + :wrap(ui.Wrap.YES), + pos = { "center", w = 70, h = 10 }, + }) + + if confirmed_save_password then + if device.uuid then + -- case hard drive + save_password(password, device.scheme, device.uuid, device.uuid) + else + local scheme, domain, user, _, prefix, port, _service_domain = extract_info_from_uri(device.uri) + save_password( + password, + scheme, + username or user, + domain, + prefix, + port, + service_domain or (username or user or ""):match("^([^;]+);") or _service_domain + ) + end + end + end + return true + elseif res and res.status.code == 2 then + if res.stderr:match(".*volume doesn’t implement mount.*") then + error_msg = string.format(NOTIFY_MSG.HEADLESS_DETECTED) + retries = max_retry + end + if res.stderr:match(".*is already mounted.*") then + return true + end + + local stdout = res.stdout + if stdout:find("\nUser: \n") or stdout:find("\nUser %[.*%]: \n") then + if retries < max_retry then + username, _ = show_input( + "Enter username " + .. (device.name and ("(" .. device.name .. ")") or (device.uri and ("(" .. device.uri .. ")") or "")) + .. ":", + false, + username or stdout:match("User %[(.*)%]:") or "" + ) + if username == nil then + return false + elseif username == "" then + -- Case using anonymous user + anonymous = true + end + else + error_msg = string.format( + NOTIFY_MSG.MOUNT_ERROR_USERNAME, + (device.name or "NO_NAME") .. " (" .. (device.scheme or "UNKNOWN_SCHEME") .. ")" + ) + end + end + if + (device.scheme == SCHEME.SMB or device.scheme == SCHEME.DNS_SD or device.scheme == SCHEME.DAVSD) + and ( + stdout:find("\nDomain: \n") + or stdout:find("\nDomain %[.*%]: \n") + or stdout:find("\nUser: \n") + or stdout:find("\nUser %[.*%]: \n") + ) + then + if retries < max_retry then + service_domain, _ = show_input( + "Enter Domain " + .. (device.name and ("(" .. device.name .. ")") or (device.uri and ("(" .. device.uri .. ")") or "")) + .. ":", + false, + service_domain or stdout:match("Domain %[(.*)%]:") or "WORKGROUP" + ) + if service_domain == nil then + return false + end + else + error_msg = string.format( + NOTIFY_MSG.MOUNT_ERROR_USERNAME, + (device.name or "NO_NAME") .. " (" .. device.scheme .. ")" + ) + end + end + if + not anonymous + and ( + stdout:find("Password: \n") + or stdout:find("\nUser: \n") + or stdout:find("\nUser %[.*%]: \n") + or stdout:find("\nDomain: \n") + or stdout:find("\nDomain %[.*%]: \n") + ) + then + if username ~= opts.username or (username == nil and is_pw_saved == nil) then + -- Prevent showing gpg passphrase twice + if not skipped_secret_vault and not is_secret_vault_available(true) then + skipped_secret_vault = true + end + if not skipped_secret_vault then + if device.uuid then + -- case hard drive + password = lookup_password(device.scheme, device.uuid, device.uuid) + else + local scheme, domain, user, _, prefix, port, _service_domain = extract_info_from_uri(device.uri) + password = lookup_password( + scheme, + username or user, + domain, + prefix, + port, + service_domain or (username or user or ""):match("^([^;]+);") or _service_domain + ) + end + is_pw_saved = password ~= nil + if is_pw_saved then + info(NOTIFY_MSG.RETRIVE_PASSWORD_SUCCESS) + end + end + end + if retries < max_retry then + if not is_pw_saved then + password, _ = show_input( + "Enter password " + .. (device.name and ("(" .. device.name .. ")") or (device.uri and ("(" .. device.uri .. ")") or "")) + .. ":", + true + ) + if password == nil then + return false + end + end + else + error_msg = string.format( + NOTIFY_MSG.MOUNT_ERROR_PASSWORD, + (device.name or "NO_NAME") .. " (" .. device.scheme .. ")" + ) + end + end + end + -- show notification after get max retry + if retries >= max_retry then + error( + tostring(error_msg or (res and not res.status.success and res.stderr) or err or "Error: Unknown"):gsub( + "%%", + "%%%%" + ) + ) + return false + end + + -- Increase retries every run + retries = retries + 1 + return mount_device({ + device = device, + retries = retries, + max_retry = max_retry, + password = password, + is_pw_saved = is_pw_saved, + skipped_secret_vault = skipped_secret_vault, + username = username, + service_domain = service_domain, + anonymous = anonymous, + }) +end + +--- Return list of connected devices +---@return Device[] +local function list_gvfs_device() + ---@type Device[] + local devices = {} + local _, res = run_command("gio", { "mount", "-li" }) + if res and res.status then + if res.status.success then + devices = parse_devices(res.stdout) + end + end + return devices +end + +---Return list of mounted devices +---@param status DEVICE_CONNECT_STATUS +---@param filter? function +---@return Device[] +local function list_gvfs_device_by_status(status, filter) + local devices = list_gvfs_device() + + local devices_filtered = {} + for _, d in ipairs(devices) do + if filter and not filter(d) then + goto continue + end + local mounted = is_mounted(d) + + if status == DEVICE_CONNECT_STATUS.MOUNTED and mounted then + table.insert(devices_filtered, d) + end + if status == DEVICE_CONNECT_STATUS.NOT_MOUNTED and not mounted then + table.insert(devices_filtered, d) + end + ::continue:: + end + return devices_filtered +end + +--- Unmount a mounted device/uri +---@param device Device +---@param eject boolean? eject = true if user want to safty unplug the device +---@param force boolean? Ignore outstanding file operations when unmounting or ejecting +---@return boolean +local function unmount_gvfs(device, eject, force, max_retry, retries) + if not device then + return true + end + max_retry = max_retry or 3 + retries = retries or 0 + + local unmount_method = "-u" + if eject then + unmount_method = "-e" + end + for _, mount in ipairs(device.mounts ~= nil and device.mounts or { device }) do + local cmd_err, res = + run_command("gio", tbl_remove_empty({ "mount", unmount_method, force and "-f" or nil, mount.uri })) + if cmd_err or (res and not res.status.success) then + if eject and res and res.stderr:find("mount doesn.*t implement .*eject.* or .*eject_with_operation.*") then + return unmount_gvfs(device, false, force) + end + if retries >= max_retry then + error(NOTIFY_MSG.UNMOUNT_ERROR, tostring(res and (res.stderr or res.stdout))) + return false + end + return unmount_gvfs(device, eject, force, max_retry, retries + 1) + end + if not cmd_err and res and res.status.success then + if eject then + info(NOTIFY_MSG.EJECT_SUCCESS, mount.name) + else + info(NOTIFY_MSG.UNMOUNT_SUCCESS, mount.name) + end + end + return true + end +end + +---show which key to select device from list +---@param devices Device|Mount[] +---@return number|nil +local function select_device_which_key(devices) + local which_keys = get_state(STATE_KEY.WHICH_KEYS) + or "1234567890qwertyuiopasdfghjklzxcvbnm-=[]\\;',./!@#$%^&*()_+{}|:\"<>?" + local allow_key_array = string_to_array(which_keys) + local cands = {} + + for idx, d in ipairs(devices) do + if idx > #allow_key_array then + break + end + table.insert(cands, { + on = tostring(allow_key_array[idx]), + desc = (d.name or "NO_NAME") .. " (" .. (d.scheme or "UNKNOWN_SCHEME") .. ")", + }) + end + + if #cands == 0 then + return + end + local selected_idx = ya.which({ + cands = cands, + }) + + if selected_idx and selected_idx > 0 then + return selected_idx + end +end + +---@param path string +---@return string? +local function get_gio_uri_from_local_path(path) + local root_mountpoint = get_state(STATE_KEY.ROOT_MOUNTPOINT) + if + not path:match("^" .. is_literal_string(root_mountpoint) .. "(.+)$") + and not path:match("^" .. is_literal_string(GVFS_ROOT_MOUNTPOINT_FILE) .. "(.+)$") + then + return nil + end + local path_info, err = Command(SHELL) + :arg({ + "-c", + "gio info " .. path_quote(path) .. ' | grep "^uri:"', + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :env("LC_ALL", "C") + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :output() + if not path_info or err or (path_info and path_info.status and not path_info.status.success) then + return nil + end + local path_uri = path_info.stdout:match("^uri: (.+)$"):gsub("\n", "") + if not path_uri then + return nil + end + + return path_uri +end + +---@param path string +---@param state_key STATE_KEY.CACHED_LOCAL_PATH_DEVICE|STATE_KEY.AUTOMOUNTS|string +---@param devices Device[]? +---@return Device? +local function get_device_from_local_path(path, state_key, devices) + local local_path = path:match("^" .. is_literal_string(get_state(STATE_KEY.ROOT_MOUNTPOINT)) .. "/[^/]+") + or path:match("^" .. is_literal_string(GVFS_ROOT_MOUNTPOINT_FILE) .. "/[^/]+") + -- NOTE: Get cached gio uri or get it from command "gio info" + ---@type Device? + local cached_device = get_state(state_key)[local_path] + local device_props_to_compare = { + "class", + "label", + "scheme", + "encrypted_uuid", + "service_domain", + "uri", + "remote_path", + } + local mount_props_to_compare = { + "class", + "uri", + "scheme", + "uuid", + "remote_path", + "name", + } + if cached_device then + if not devices then + devices = list_gvfs_device() + end + local matched_device = nil + for _, device in ipairs(devices) do + matched_device = device + for _, prop in ipairs(device_props_to_compare) do + if cached_device[prop] ~= device[prop] then + matched_device = nil + goto jump_to_compare_mounts + end + end + if matched_device then + return matched_device + end + ::jump_to_compare_mounts:: + for _, mount in ipairs(device.mounts) do + matched_device = device + for _, prop in ipairs(mount_props_to_compare) do + if cached_device[prop] ~= mount[prop] then + matched_device = nil + break + end + end + end + end + return matched_device + else + local gio_uri = get_gio_uri_from_local_path(path) + if not gio_uri then + return nil + end + if not devices then + devices = list_gvfs_device() + end + for _, device in ipairs(devices) do + if device.uri and gio_uri:match("^" .. is_literal_string(device.uri) .. ".*") then + return device + end + for _, mount in ipairs(device.mounts) do + if mount.uri and gio_uri:match("^" .. is_literal_string(mount.uri) .. ".*") then + return device + end + end + end + return nil + end + return nil +end + +--- Jump to device mountpoint +---@param device Device? +local function jump_to_device_mountpoint_action(device, retry, automount) + if automount then + -- Trigger Automount + local automount_script = HOME .. "/.config/yazi/plugins/gvfs.yazi/assets/automount.sh" + run_command("chmod", { "+x", automount_script }) + run_command(automount_script, {}) + end + if not device then + local list_devices = list_gvfs_device_by_status(DEVICE_CONNECT_STATUS.MOUNTED) + device = #list_devices == 1 and list_devices[1] or nil + if not device then + local selected_device_idx = select_device_which_key(list_devices) + if not selected_device_idx then + return + end + device = list_devices[selected_device_idx] + end + end + if not device then + info(NOTIFY_MSG.LIST_DEVICES_EMPTY) + return + end + local mnt_path = get_mounted_path(device) + if not mnt_path and not retry then + -- case hard drive encrypted -> mount uuid changed + local matched_devices = list_gvfs_device_by_status(DEVICE_CONNECT_STATUS.MOUNTED, function(d) + return (device.uuid and (d.encrypted_uuid == device.uuid or d.uuid == device.uuid)) + or (device.uri and d.uri == device.uri) + end) + if #matched_devices >= 1 then + device = matched_devices[1] + return jump_to_device_mountpoint_action(device, true, automount) + end + end + + if mnt_path then + set_state(STATE_KEY.PREV_CWD, current_dir()) + if device.remote_path then + mnt_path = pathJoin(mnt_path, device.remote_path) + end + ya.emit("cd", { mnt_path, raw = true }) + else + error(NOTIFY_MSG.DEVICE_IS_DISCONNECTED) + end +end + +--- Jump to previous directory +local function jump_to_prev_cwd_action() + local prev_cwd = get_state(STATE_KEY.PREV_CWD) + if not prev_cwd then + return + end + if is_dir(prev_cwd) then + set_state(STATE_KEY.PREV_CWD, current_dir()) + ya.emit("cd", { prev_cwd, raw = true }) + else + error(NOTIFY_MSG.CANT_ACCESS_PREV_CWD) + end +end + +--- mount action +---@param opts { jump: boolean?, device: Device? }? +local function mount_action(opts) + local selected_device + -- Let user select a device if device is not specified + if not opts or not opts.device then + local list_devices = list_gvfs_device_by_status(DEVICE_CONNECT_STATUS.NOT_MOUNTED, function(d) + return true + end) + -- NOTE: Automatically select the first device if there is only one device + selected_device = #list_devices == 1 and list_devices[1] or nil + if not selected_device then + local selected_device_idx = select_device_which_key(list_devices) + if not selected_device_idx then + return + end + selected_device = list_devices[selected_device_idx] + end + + if #list_devices == 0 then + -- If every devices are mounted, then jump to the first one + local root_mountpoint = get_state(STATE_KEY.ROOT_MOUNTPOINT) + local list_devices_mounted = list_gvfs_device_by_status(DEVICE_CONNECT_STATUS.MOUNTED, function(d) + if d.scheme == SCHEME.FILE then + return ( + d.mounts + and #d.mounts >= 1 + and d.mounts[1].uri + and ( + d.mounts[1].uri:match("^" .. is_literal_string("file://" .. root_mountpoint) .. "(.+)$") + or d.mounts[1].uri:match( + "^" .. is_literal_string("file://" .. GVFS_ROOT_MOUNTPOINT_FILE) .. "(.+)$" + ) + ) + ) + end + return true + end) + selected_device = #list_devices_mounted >= 1 and list_devices_mounted[1] or nil + if not selected_device then + info(NOTIFY_MSG.LIST_DEVICES_EMPTY) + return + end + --NOTE: Fall-safe x-gvfs-show + local status, err = Command(SHELL) + :arg({ + "-c", + "gio info " .. path_quote( + selected_device.uri or (#selected_device.mounts > 0 and selected_device.mounts[1].uri) + ) .. ' | grep -E "^unix mount:.*x-gvfs-show.*"', + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :env("LC_ALL", "C") + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :status() + if err then + error(NOTIFY_MSG.UNMOUNT_ERROR, tostring(err)) + return + end + if status and status.code == 0 then + error(NOTIFY_MSG.UNMOUNT_ERROR, "Can't unmount device mounted through /etc/fstab") + return + end + + jump_to_device_mountpoint_action(selected_device) + return true + end + else + selected_device = opts.device + end + if not selected_device then + return + end + + local success = mount_device({ + device = selected_device, + }) + + if success and opts and opts.jump then + jump_to_device_mountpoint_action(selected_device) + end + return success +end + +local save_tab_hovered = ya.sync(function() + local hovered_item_per_tab = {} + for _, tab in ipairs(cx.tabs) do + local is_virtual = Url(tab.current.cwd).scheme and Url(tab.current.cwd).scheme.is_virtual + table.insert(hovered_item_per_tab, { + id = (type(tab.id) == "number" or type(tab.id) == "string") and tab.id or tab.id.value, + cwd = tostring((is_virtual and tab.current.cwd or tab.current.cwd.path) or tab.current.cwd), + }) + end + return hovered_item_per_tab +end) + +local redirect_unmounted_tab_to_home = ya.sync(function(_, unmounted_url, notify) + if not unmounted_url or unmounted_url == "" then + return + end + -- broadcast to other instances + if notify then + broadcast(PUBSUB_KIND.unmounted, hex_encode(unmounted_url)) + end + for _, tab in ipairs(cx.tabs) do + local is_virtual = Url(tab.current.cwd).scheme and Url(tab.current.cwd).scheme.is_virtual + if ((is_virtual and tab.current.cwd or tab.current.cwd.path) or tab.current.cwd):starts_with(unmounted_url) then + ya.emit("cd", { + HOME, + tab = (type(tab.id) == "number" or type(tab.id) == "string") and tab.id or tab.id.value, + raw = true, + }) + end + end +end) + +--- unmount action +--- @param device Device? +--- @param eject boolean? eject = true if user want to safty unplug the device +--- @param force boolean? Ignore outstanding file operations when unmounting or ejecting +local function unmount_action(device, eject, force) + local selected_device + if not device then + local root_mountpoint = get_state(STATE_KEY.ROOT_MOUNTPOINT) + + local list_devices = list_gvfs_device_by_status(DEVICE_CONNECT_STATUS.MOUNTED, function(d) + if d.scheme == SCHEME.FILE then + return ( + d.mounts + and #d.mounts >= 1 + and d.mounts[1].uri + and ( + d.mounts[1].uri:match("^" .. is_literal_string("file://" .. root_mountpoint) .. "(.+)$") + or d.mounts[1].uri:match( + "^" .. is_literal_string("file://" .. GVFS_ROOT_MOUNTPOINT_FILE) .. "(.+)$" + ) + ) + ) + end + return true + end) + -- NOTE: Automatically select the first device if there is only one device + selected_device = #list_devices == 1 and list_devices[1] or nil + if not selected_device then + local selected_device_idx = select_device_which_key(list_devices) + if not selected_device_idx then + return + end + selected_device = list_devices[selected_device_idx] + end + + if not selected_device and #list_devices == 0 then + info(NOTIFY_MSG.LIST_DEVICES_EMPTY) + return + end + --NOTE: Fall-safe x-gvfs-show + if selected_device then + local status, err = Command(SHELL) + :arg({ + "-c", + "gio info " .. path_quote( + selected_device.uri or (#selected_device.mounts > 0 and selected_device.mounts[1].uri) + ) .. ' | grep -E "^unix mount:.*x-gvfs-show.*"', + }) + :env("XDG_RUNTIME_DIR", XDG_RUNTIME_DIR) + :env("LC_ALL", "C") + :stderr(Command.PIPED) + :stdout(Command.PIPED) + :status() + if err then + error(NOTIFY_MSG.UNMOUNT_ERROR, tostring(err)) + return + end + if status and status.code == 0 then + error(NOTIFY_MSG.UNMOUNT_ERROR, "Can't unmount device mounted through /etc/fstab") + return + end + end + end + if device then + selected_device = device + end + if not selected_device then + return + end + + local mount_path = get_mounted_path(selected_device) + if selected_device.uuid and mount_path then + redirect_unmounted_tab_to_home(mount_path, true) + end + local success = unmount_gvfs(selected_device, eject, force) + if success and not selected_device.uuid and mount_path then + redirect_unmounted_tab_to_home(mount_path, true) + -- cd to home for all tabs within the device, and then restore the tabs location + end +end + +---@param state_key STATE_KEY.CACHED_LOCAL_PATH_DEVICE|STATE_KEY.AUTOMOUNTS|string +---@param jump_location string? +---@param tab_id number? +local function remount_keep_cwd_unchanged_action(state_key, jump_location, tab_id, hide_cant_remount_message) + local cwd = jump_location or current_dir() + local root_mountpoint = get_state(STATE_KEY.ROOT_MOUNTPOINT) + if + not cwd:match("^" .. is_literal_string(root_mountpoint) .. "(.+)$") + and not cwd:match("^" .. is_literal_string(GVFS_ROOT_MOUNTPOINT_FILE) .. "(.+)$") + then + return nil + end + + local devices = list_gvfs_device() + local current_tab_device = get_device_from_local_path(cwd, state_key, devices) + if not current_tab_device then + info(NOTIFY_MSG.DEVICE_IS_DISCONNECTED) + return + end + if is_mounted(current_tab_device) then + if not hide_cant_remount_message then + info(NOTIFY_MSG.CANT_REMOUNT_DEVICE, current_tab_device.name) + end + return current_tab_device + end + local tabs = save_tab_hovered() + local saved_matched_tabs = {} + -- cd to home for all tabs within the device, and then restore the tabs location + if state_key == STATE_KEY.CACHED_LOCAL_PATH_DEVICE then + for _, tab in ipairs(tabs) do + local tab_device = get_device_from_local_path(tostring(tab.cwd), state_key, devices) + if tab_device and tab_device.name == current_tab_device.name then + table.insert(saved_matched_tabs, tab) + ya.emit("cd", { + root_mountpoint, + tab = tab.id, + raw = true, + }) + end + end + end + mount_action({ jump = false, device = current_tab_device }) + if state_key == STATE_KEY.CACHED_LOCAL_PATH_DEVICE then + for _, tab in ipairs(saved_matched_tabs) do + ya.emit("cd", { + tostring(tab.cwd), + tab = tab.id, + raw = true, + }) + end + else + if jump_location then + local jump_location_cha, _ = fs.cha(Url(jump_location)) + ya.emit((jump_location_cha and jump_location_cha.is_dir) and "cd" or "reveal", { + tostring(jump_location), + no_dummy = true, + raw = true, + tab = tab_id, + }) + end + end + return current_tab_device +end + +local save_automount_devices = function() + local automounts = get_state(STATE_KEY.AUTOMOUNTS) + + local save_path = Url(get_state(STATE_KEY.SAVE_PATH_AUTOMOUNTS)) + -- create parent directories + local save_path_created, err_create = fs.create("dir_all", save_path.parent) + + if err_create then + error(NOTIFY_MSG.CANT_CREATE_SAVE_FOLDER, tostring(save_path.parent)) + end + + -- save mounts to file + if save_path_created then + local _, err_write = fs.write(save_path, ya.json_encode(hex_encode_table(automounts))) + if err_write then + error(NOTIFY_MSG.CANT_SAVE_DEVICES, tostring(save_path)) + end + end + + -- trigger update to other instances + broadcast(PUBSUB_KIND.automounts_changed, hex_encode_table(automounts)) +end + +local save_mounts = function() + local mounts = get_state(STATE_KEY.MOUNTS) + local mounts_to_save = {} + for idx = #mounts, 1, -1 do + if mounts[idx].is_manually_added then + -- save name, uri, scheme, is_manually_added + table.insert(mounts_to_save, 1, { + name = mounts[idx].name, + uri = mounts[idx].uri, + scheme = mounts[idx].scheme, + is_manually_added = mounts[idx].is_manually_added, + }) + end + end + + local save_path = Url(get_state(STATE_KEY.SAVE_PATH)) + -- create parent directories + local save_path_created, err_create = fs.create("dir_all", save_path.parent) + + if err_create then + error(NOTIFY_MSG.CANT_CREATE_SAVE_FOLDER, tostring(save_path.parent)) + end + + -- save mounts to file + if save_path_created then + local _, err_write = fs.write(save_path, ya.json_encode(hex_encode_table(mounts))) + if err_write then + error(NOTIFY_MSG.CANT_SAVE_DEVICES, tostring(save_path)) + end + end + + -- trigger update to other instances + broadcast(PUBSUB_KIND.mounts_changed, hex_encode_table(mounts)) +end + +local read_hex_decoded_file_content = function(save_path) + local file = io.open(save_path, "r") + if file == nil then + return {} + end + local encoded_data = file:read("*all") + file:close() + return hex_decode_table(ya.json_decode(encoded_data)) +end + +---@param is_edit boolean? +local function add_or_edit_mount_action(is_edit) + ---@type any + local mount = { + is_manually_added = true, + } + + local selected_idx = nil + + if is_edit then + local mounts = get_state(STATE_KEY.MOUNTS) + if #mounts == 0 then + info(NOTIFY_MSG.LIST_MOUNTS_EMPTY) + return + end + selected_idx = select_device_which_key(mounts) + if not selected_idx then + return + end + mount = tbl_deep_clone(mounts[selected_idx]) + end + + mount.uri, _ = show_input("Enter mount URI:", false, mount.uri) + if mount.uri == nil then + return + end + mount.uri = mount.uri:gsub("^%s*(.-)%s*$", "%1") + if mount.uri == nil then + return + elseif mount.uri == "" then + error(NOTIFY_MSG.URI_CANT_BE_EMPTY) + end + mount.uri = mount.uri:gsub("/$", "") + -- sftp://test@192.168.1.2 + -- ftp://huyhoang@192.168.1.2:9999/ + local _scheme, uri = string.match(mount.uri, "([^:]+)://(.+)") + local scheme + if not _scheme or not uri then + error(NOTIFY_MSG.URI_IS_INVALID) + return + end + for _, value in pairs(SCHEME) do + if _scheme == value and value ~= SCHEME.FILE then + scheme = value + end + end + + mount.scheme = scheme + if not scheme then + error(NOTIFY_MSG.UNSUPPORTED_SCHEME, tostring(_scheme)) + return + end + if scheme == SCHEME.GOOGLE_DRIVE or scheme == SCHEME.ONE_DRIVE then + error(NOTIFY_MSG.UNSUPPORTED_MANUALLY_MOUNT_SCHEME, tostring(_scheme)) + return + end + if scheme == SCHEME.SMB then + mount.service_domain = mount.uri:match("^smb://([^;]+);") + if not mount.service_domain then + mount.service_domain, _ = show_input("Enter SMB domain:", false, "WORKGROUP") + end + if not mount.service_domain then + return + end + end + mount.name, _ = show_input("Enter display name:", false, mount.name or uri) + + if mount.name == nil then + return + end + + if mount.name == "" or not mount.name then + error(NOTIFY_MSG.DISPLAY_NAME_CANT_BE_EMPTY) + return + end + + local mounts = get_state(STATE_KEY.MOUNTS) + if selected_idx then + if is_mounted(mounts[selected_idx]) then + unmount_action(mounts[selected_idx], false, true) + end + if mount.uri ~= mounts[selected_idx].uri then + local old_scheme, old_domain, old_user, _, old_prefix, old_port, old_service_domain = + extract_info_from_uri(mounts[selected_idx].uri) + if old_domain and old_scheme and is_secret_vault_available(true) then + clear_password( + old_scheme, + old_user, + old_domain, + old_prefix, + old_port, + old_service_domain or mounts[selected_idx].service_domain + ) + end + end + mounts[selected_idx] = mount + info(NOTIFY_MSG.UPDATED_MOUNT_URI, mount.name) + else + table.insert(mounts, mount) + info(NOTIFY_MSG.ADDED_MOUNT_URI, mount.name) + end + set_state(STATE_KEY.MOUNTS, mounts) + save_mounts() +end + +local function load_gdrive_folder_action() + if + get_state(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER_RUNNING) + or not get_state(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER) + or #get_state(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER) == 0 + then + return + end + set_state(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER_RUNNING, true) + local data = dequeue_task(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER) + local folder_to_load_raw = data.folder_to_load + local self_load = data.self_load + local folder_to_load = Url(folder_to_load_raw) + + local gdrive_mountpoint_children_info = get_gdrive_children_folder_info(folder_to_load_raw) + if gdrive_mountpoint_children_info then + display_virtual_children(folder_to_load, gdrive_mountpoint_children_info) + end + -- TODO: parent + if self_load then + -- ya.sleep(0.5) + -- if folder_to_load.parent and folder_to_load.parent.parent then + -- local gdrive_children_info_parents = get_gdrive_children_folder_info(folder_to_load.parent.parent) + -- if gdrive_children_info_parents then + -- display_virtual_children(folder_to_load.parent.parent, gdrive_children_info_parents) + -- end + -- end + -- TODO: Load preview + -- ya.sleep(0.5) + -- local hovered_folder_cwd = current_hovered_folder_cwd() + -- if hovered_folder_cwd then + -- local gdrive_children_info_parents = get_gdrive_children_folder_info(hovered_folder_cwd) + -- if gdrive_children_info_parents then + -- display_virtual_children(hovered_folder_cwd, gdrive_children_info_parents) + -- end + -- end + end + -- ya.sleep(0.5) + set_state(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER_RUNNING, false) + load_gdrive_folder_action() +end + +local function cache_local_path_device_action(local_path) + local device_matched = get_device_from_local_path(local_path, STATE_KEY.CACHED_LOCAL_PATH_DEVICE) + if device_matched then + set_state_table(STATE_KEY.CACHED_LOCAL_PATH_DEVICE, local_path, device_matched) + end +end + +---@param enabled boolean? +local function toggle_automount_when_cd_action(enabled) + local hovered_path = get_hovered_path() + local is_virtual = Url(hovered_path).scheme and Url(hovered_path).scheme.is_virtual + if is_virtual then + return + end + local local_path = hovered_path:match("^" .. is_literal_string(get_state(STATE_KEY.ROOT_MOUNTPOINT)) .. "/[^/]+") + or hovered_path:match("^" .. is_literal_string(GVFS_ROOT_MOUNTPOINT_FILE) .. "/[^/]+") + if local_path then + if not enabled then + set_state_table(STATE_KEY.AUTOMOUNTS, local_path, nil) + else + local device_matched = get_device_from_local_path(local_path, STATE_KEY.AUTOMOUNTS) + if device_matched then + if device_matched.mounts and #device_matched.mounts > 0 and not can_device_umount(device_matched) then + info(NOTIFY_MSG.CANT_AUTOMOUNT, device_matched.name) + return + end + -- + set_state_table(STATE_KEY.AUTOMOUNTS, local_path, device_matched) + end + end + info(NOTIFY_MSG.AUTOMOUNT_WHEN_CD_STATE, enabled and "Enabled" or "Disabled", tostring(local_path)) + save_automount_devices() + end +end +local function remove_mount_action() + local mounts = get_state(STATE_KEY.MOUNTS) + if #mounts == 0 then + info(NOTIFY_MSG.LIST_MOUNTS_EMPTY) + return + end + + local selected_idx = select_device_which_key(mounts) + if not selected_idx then + return + end + + local mount = mounts[selected_idx] + if not mount then + return + end + + if is_mounted(mount) then + unmount_action(mount, false, true) + end + -- run_command("gio", { "mount", "-u", mount.uri }) + local old_scheme, old_domain, old_user, _, old_prefix, old_port, old_service_domain = + extract_info_from_uri(mounts[selected_idx].uri) + if old_domain and old_scheme and is_secret_vault_available(true) then + clear_password( + old_scheme, + old_user, + old_domain, + old_prefix, + old_port, + old_service_domain or mount.service_domain + ) + end + local removed_mount = table.remove(mounts, selected_idx) + set_state(STATE_KEY.MOUNTS, mounts) + info(NOTIFY_MSG.REMOVED_MOUNT_URI, removed_mount.name) + save_mounts() +end + +---setup function in yazi/init.lua +---@param opts {} +function M:setup(opts) + local st = self + + if opts and opts.key_grip then + st[STATE_KEY.KEY_GRIP] = opts.key_grip + end + st[STATE_KEY.INPUT_POSITION] = (opts and type(opts.input_position) == "table") and opts.input_position + or { "top-center", y = 3, w = 60 } + + if opts and opts.save_password_autoconfirm == true then + st[STATE_KEY.SAVE_PASSWORD_AUTOCONFIRM] = true + end + if opts and opts.password_vault then + st[STATE_KEY.PASSWORD_VAULT] = ( + opts and (opts.password_vault == PASSWORD_VAULT.KEYRING or opts.password_vault == PASSWORD_VAULT.PASS) + ) and opts.password_vault + else + -- TODO: REMOVE: backwards compatibility + if opts and opts.enabled_keyring == true then + st[STATE_KEY.PASSWORD_VAULT] = PASSWORD_VAULT.KEYRING + end + end + + if opts and opts.which_keys and type(opts.which_keys) == "string" then + st[STATE_KEY.WHICH_KEYS] = opts.which_keys + end + local save_path = os.getenv("HOME") .. "/.config/yazi/gvfs.private" + if type(opts) == "table" then + save_path = opts.save_path or save_path + end + + local save_path_automounts = os.getenv("HOME") .. "/.config/yazi/gvfs_automounts.private" + if type(opts) == "table" then + save_path_automounts = opts.save_path_automounts or save_path_automounts + end + st[STATE_KEY.SAVE_PATH] = save_path + st[STATE_KEY.SAVE_PATH_AUTOMOUNTS] = save_path_automounts + + -- NOTE: Use pathJoin to avoid double slashes and end with a slash + if opts and opts.root_mountpoint and type(opts.root_mountpoint) == "string" then + st[STATE_KEY.ROOT_MOUNTPOINT] = pathJoin(opts.root_mountpoint) + else + st[STATE_KEY.ROOT_MOUNTPOINT] = pathJoin(GVFS_ROOT_MOUNTPOINT) + end + st[STATE_KEY.BLACKLIST_DEVICES] = (opts and opts.blacklist_devices and type(opts.blacklist_devices) == "table") + and opts.blacklist_devices + or {} + + st[STATE_KEY.MOUNTS] = read_hex_decoded_file_content(get_state(STATE_KEY.SAVE_PATH)) + st[STATE_KEY.AUTOMOUNTS] = read_hex_decoded_file_content(get_state(STATE_KEY.SAVE_PATH_AUTOMOUNTS)) or {} + + st[STATE_KEY.CACHED_LOCAL_PATH_DEVICE] = {} + + ps.sub(PUBSUB_KIND.cd, function(payload) + local cwd = cx.active.current.cwd + if not cwd then + return + end + + local cwd_raw = tostring(cwd) + if + cwd_raw:match( + "^" .. is_literal_string(get_state(STATE_KEY.ROOT_MOUNTPOINT) .. "/google-drive:host=gmail.com") + ) + then + enqueue_task(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER, { folder_to_load = cwd_raw, self_load = true }) + local args = ya.quote(ACTION.LOAD_GDRIVE_FOLDER) + ya.emit("plugin", { + self._id, + args, + }) + end + local local_path = cwd_raw:match("^" .. is_literal_string(st[STATE_KEY.ROOT_MOUNTPOINT]) .. "/[^/]+") + or cwd_raw:match("^" .. is_literal_string(GVFS_ROOT_MOUNTPOINT_FILE) .. "/[^/]+") + + if local_path then + if not st[STATE_KEY.CACHED_LOCAL_PATH_DEVICE][local_path] then + local args = ya.quote(ACTION.CACHE_LOCAL_PATH_DEVICE) .. " " .. ya.quote(local_path) + ya.emit("plugin", { + self._id, + args, + }) + end + if st[STATE_KEY.AUTOMOUNTS][local_path] and not st[STATE_KEY.AUTOMOUNTS][local_path].locked_automount then + st[STATE_KEY.AUTOMOUNTS][local_path].locked_automount = true + local args = ya.quote(ACTION.MOUNT_THEN_JUMP_SUBFOLDER) + .. " " + .. ya.quote(cwd_raw) + .. " " + .. ya.quote(local_path) + .. " " + .. ya.quote(payload.tab) + ya.emit("plugin", { + self._id, + args, + }) + end + end + end) + + -- ps.sub(PUBSUB_KIND.hover, function() + -- local cwd = current_hovered_folder_cwd() + -- if not cwd then + -- return + -- end + -- local cwd_raw = tostring(cwd) + -- if + -- cwd_raw:match( + -- "^" .. is_literal_string(get_state(STATE_KEY.ROOT_MOUNTPOINT) .. "/google-drive:host=gmail.com") + -- ) + -- then + -- enqueue_task(STATE_KEY.TASKS_LOAD_GDRIVE_FOLDER, { folder_to_load = cwd_raw, self_load = false }) + -- local args = ya.quote(ACTION.LOAD_GDRIVE_FOLDER) + -- ya.emit("plugin", { + -- self._id, + -- args, + -- }) + -- end + -- end) + ps.sub_remote(PUBSUB_KIND.mounts_changed, function(mounts) + set_state(STATE_KEY.MOUNTS, hex_decode_table(mounts)) + end) + + ps.sub_remote(PUBSUB_KIND.automounts_changed, function(automounts) + set_state(STATE_KEY.AUTOMOUNTS, hex_decode_table(automounts)) + end) + + ps.sub_remote(PUBSUB_KIND.unmounted, function(unmounted_url) + redirect_unmounted_tab_to_home(hex_decode(unmounted_url)) + end) +end + +---@param job {args: unknown[], args: {jump: boolean?, eject: boolean?, force: boolean?, automount: boolean?, disabled: boolean?}} +function M:entry(job) + if not is_cmd_exist("gio") then + error(NOTIFY_MSG.CMD_NOT_FOUND, "gio") + return + end + -- Fallback to pass if in headless session + if not is_in_dbus_session() then + error(NOTIFY_MSG.HEADLESS_DETECTED) + return + end + + if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.KEYRING then + if not is_cmd_exist(SECRET_TOOL) then + set_state(STATE_KEY.PASSWORD_VAULT, nil) + end + end + if get_state(STATE_KEY.PASSWORD_VAULT) == PASSWORD_VAULT.PASS then + if not is_cmd_exist(GPG_TOOL) or not is_cmd_exist(PASS_TOOL) or get_state(STATE_KEY.KEY_GRIP) == nil then + set_state(STATE_KEY.PASSWORD_VAULT, nil) + end + end + local action = job.args[1] + -- Select a device then mount + if action == ACTION.SELECT_THEN_MOUNT then + local jump = job.args.jump or false + mount_action({ jump = jump }) + -- select a device then unmount + elseif action == ACTION.SELECT_THEN_UNMOUNT then + local eject = job.args.eject or false + local force = job.args.force or false + unmount_action(nil, eject, force) + -- remount device within current cwd + elseif action == ACTION.REMOUNT_KEEP_CWD_UNCHANGED then + remount_keep_cwd_unchanged_action(STATE_KEY.CACHED_LOCAL_PATH_DEVICE) + -- select a device then go to its mounted point + elseif action == ACTION.JUMP_TO_DEVICE then + local automount = job.args.automount or false + jump_to_device_mountpoint_action(nil, nil, automount) + elseif action == ACTION.JUMP_BACK_PREV_CWD then + jump_to_prev_cwd_action() + elseif action == ACTION.ADD_MOUNT then + add_or_edit_mount_action() + elseif action == ACTION.EDIT_MOUNT then + add_or_edit_mount_action(true) + elseif action == ACTION.REMOVE_MOUNT then + remove_mount_action() + elseif action == ACTION.AUTOMOUNT_WHEN_CD then + local enabled = not job.args.disabled + toggle_automount_when_cd_action(enabled) + + -- NOTE: Switch to new async when it's merged to yazi nightly + elseif action == ACTION.LOAD_GDRIVE_FOLDER then + load_gdrive_folder_action() + elseif action == ACTION.CACHE_LOCAL_PATH_DEVICE then + local local_path = job.args[2] + if fs.cha(Url(local_path)) then + cache_local_path_device_action(local_path) + end + elseif action == ACTION.MOUNT_THEN_JUMP_SUBFOLDER then + local subfolder_path = job.args[2] + local local_path = job.args[3] + local tab_id = job.args[4] + local local_path_cha, _ = fs.cha(Url(local_path)) + local cached_device = get_state(STATE_KEY.AUTOMOUNTS)[local_path] + if local_path_cha and local_path_cha.is_dir then + -- NOTE: Skip automount + cached_device.locked_automount = false + set_state_table(STATE_KEY.AUTOMOUNTS, local_path, cached_device) + return + end + if cached_device then + -- Update cached device with new data + local new_cached_device = + remount_keep_cwd_unchanged_action(STATE_KEY.AUTOMOUNTS, subfolder_path, tab_id, true) + if new_cached_device then + cached_device = new_cached_device + end + end + cached_device.locked_automount = false + set_state_table(STATE_KEY.AUTOMOUNTS, local_path, cached_device) + end + -- TODO: remove this after next yazi released + (ui.render or ya.render)() +end + +return M diff --git a/.config/yazi/plugins/jump-to-char.yazi/LICENSE b/.config/yazi/plugins/jump-to-char.yazi/LICENSE new file mode 100644 index 0000000..fb5b1d6 --- /dev/null +++ b/.config/yazi/plugins/jump-to-char.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/.config/yazi/plugins/jump-to-char.yazi/README.md b/.config/yazi/plugins/jump-to-char.yazi/README.md new file mode 100644 index 0000000..6a77faf --- /dev/null +++ b/.config/yazi/plugins/jump-to-char.yazi/README.md @@ -0,0 +1,28 @@ +# jump-to-char.yazi + +Vim-like `f`, jump to the next file whose name starts with ``. + +https://github.com/yazi-rs/plugins/assets/17523360/aac9341c-b416-4e0c-aaba-889d48389869 + +## Installation + +```sh +ya pkg add yazi-rs/plugins:jump-to-char +``` + +## Usage + +Add this to your `~/.config/yazi/keymap.toml`: + +```toml +[[mgr.prepend_keymap]] +on = "f" +run = "plugin jump-to-char" +desc = "Jump to char" +``` + +Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other actions/plugins. + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/.config/yazi/plugins/jump-to-char.yazi/main.lua b/.config/yazi/plugins/jump-to-char.yazi/main.lua new file mode 100644 index 0000000..8a434f1 --- /dev/null +++ b/.config/yazi/plugins/jump-to-char.yazi/main.lua @@ -0,0 +1,32 @@ +--- @since 25.5.31 + +local AVAILABLE_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789." + +local changed = ya.sync(function(st, new) + local b = st.last ~= new + st.last = new + return b or not cx.active.finder +end) + +local escape = function(s) return s == "." and "\\." or s end + +return { + entry = function() + local cands = {} + for i = 1, #AVAILABLE_CHARS do + cands[#cands + 1] = { on = AVAILABLE_CHARS:sub(i, i) } + end + + local idx = ya.which { cands = cands, silent = true } + if not idx then + return + end + + local kw = escape(cands[idx].on) + if changed(kw) then + ya.emit("find_do", { "^" .. kw }) + else + ya.emit("find_arrow", {}) + end + end, +} diff --git a/.config/yazi/plugins/mount.yazi/LICENSE b/.config/yazi/plugins/mount.yazi/LICENSE new file mode 100644 index 0000000..fb5b1d6 --- /dev/null +++ b/.config/yazi/plugins/mount.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/.config/yazi/plugins/mount.yazi/README.md b/.config/yazi/plugins/mount.yazi/README.md new file mode 100644 index 0000000..16b5a16 --- /dev/null +++ b/.config/yazi/plugins/mount.yazi/README.md @@ -0,0 +1,50 @@ +# mount.yazi + +A mount manager for Yazi, providing disk mount, unmount, and eject functionality. + +Supported platforms: + +- Linux with [`udisksctl`](https://github.com/storaged-project/udisks), `lsblk` and `eject` both provided by [`util-linux`](https://github.com/util-linux/util-linux) +- macOS with `diskutil`, which is pre-installed + +https://github.com/user-attachments/assets/c6f780ab-458b-420f-85cf-2fc45fcfe3a2 + +## Installation + +```sh +ya pkg add yazi-rs/plugins:mount +``` + +## Usage + +Add this to your `~/.config/yazi/keymap.toml`: + +```toml +[[mgr.prepend_keymap]] +on = "M" +run = "plugin mount" +``` + +Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other actions/plugins. + +## Actions + +| Key binding | Alternate key | Action | +| ------------ | ------------- | --------------------- | +| q | - | Quit the plugin | +| k | ↑ | Move up | +| j | ↓ | Move down | +| l | → | Enter the mount point | +| m | - | Mount the partition | +| u | - | Unmount the partition | +| e | - | Eject the disk | + +## TODO + +- Custom keybindings +- Windows support (I don't use Windows myself, PRs welcome!) +- Support mount, unmount, and eject the entire disk + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/.config/yazi/plugins/mount.yazi/cross.lua b/.config/yazi/plugins/mount.yazi/cross.lua new file mode 100644 index 0000000..758861a --- /dev/null +++ b/.config/yazi/plugins/mount.yazi/cross.lua @@ -0,0 +1,71 @@ +local M = {} + +--- @param type "mount"|"unmount"|"eject" +--- @param partition table +function M.operate(type, partition) + if not partition then + return + elseif not partition.sub then + return -- TODO: mount/unmount main disk + end + + local cmd, output, err + if ya.target_os() == "macos" then + cmd, output, err = "diskutil", M.diskutil(type, partition.src) + elseif ya.target_os() == "linux" then + if type == "eject" and partition.src:match("^/dev/sr%d+") then + M.udisksctl("unmount", partition.src) + cmd, output, err = "eject", M.eject(partition.src) + elseif type == "eject" then + M.udisksctl("unmount", partition.src) + cmd, output, err = "udisksctl", M.udisksctl("power-off", partition.src) + else + cmd, output, err = "udisksctl", M.udisksctl(type, partition.src) + end + end + + if not cmd then + M.fail("mount.yazi is not currently supported on your platform") + elseif not output then + M.fail("Failed to spawn `%s`: %s", cmd, err) + elseif not output.status.success then + M.fail("Failed to %s `%s`: %s", type, partition.src, output.stderr) + end +end + +--- @param type "mount"|"unmount"|"eject" +--- @param src string +--- @return Output? output +--- @return Error? err +function M.diskutil(type, src) return Command("diskutil"):arg({ type, src }):output() end + +--- @param type "mount"|"unmount"|"power-off" +--- @param src string +--- @return Output? output +--- @return Error? err +function M.udisksctl(type, src) + local args = { type, "-b", src, "--no-user-interaction" } + local output, err = Command("udisksctl"):arg(args):output() + + if not output or err then + return nil, err + elseif output.stderr:find("org.freedesktop.UDisks2.Error.NotAuthorizedCanObtain", 1, true) then + local tx, rx = table.unpack(require(".main").permit) + tx:send(true) + ya.emit("which:dismiss", {}) + local output, err = require(".sudo").run_with_sudo("udisksctl", args) + rx:recv() + return output, err + else + return output + end +end + +--- @param src string +--- @return Output? output +--- @return Error? err +function M.eject(src) return Command("eject"):arg({ "--traytoggle", src }):output() end + +function M.fail(...) ya.notify { title = "Mount", content = string.format(...), timeout = 10, level = "error" } end + +return M diff --git a/.config/yazi/plugins/mount.yazi/main.lua b/.config/yazi/plugins/mount.yazi/main.lua new file mode 100644 index 0000000..2cfb2a1 --- /dev/null +++ b/.config/yazi/plugins/mount.yazi/main.lua @@ -0,0 +1,272 @@ +--- @since 26.5.6 + +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 + ui.render() +end) + +local subscribe = ya.sync(function() + ps.unsub("mount") + ps.sub("mount", function() + -- TODO: use `ya.async()` + ya.emit("plugin", { "mount", "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)) + ui.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 + ui.render() +end) + +local M = { + keys = { + { on = "q", run = "quit" }, + { on = "", run = "quit" }, + { on = "", run = { "enter", "quit" } }, + + { on = "k", run = "up" }, + { on = "j", run = "down" }, + { on = "l", run = { "enter", "quit" } }, + + { on = "", run = "up" }, + { on = "", run = "down" }, + { on = "", run = { "enter", "quit" } }, + + { on = "m", run = "mount" }, + { on = "u", run = "unmount" }, + { on = "e", run = "eject" }, + }, + permit = table.pack(ya.chan("mpsc", 1)), +} + +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() + self.permit[1]:send(true) + while true do + self.permit[2]:recv() + local idx = ya.which { cands = self.keys, silent = true } + self.permit[1]:send(true) + + local cand = self.keys[idx] 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.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 + require(".cross").operate("mount", active_partition()) + elseif run == "unmount" then + require(".cross").operate("unmount", active_partition()) + elseif run == "eject" then + require(".cross").operate("eject", active_partition()) + 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.Edge.ALL) + :area(self._area) + :type(ui.Border.ROUNDED) + :style(ui.Style():fg("blue")) + :title(ui.Line("Mount"):align(ui.Align.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(20), + }, + } +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 + { "^/dev/sr%d+", ".+$" }, -- /dev/sr0 + { "^/dev/fd%d+", ".+$" }, -- /dev/fd0 + { "^/dev/md%d+", "p%d+$" }, -- /dev/md0p1 + { "^/dev/nbd%d+", "p%d+$" }, -- /dev/nbd0p1 + { "^/dev/bcache%d+", "p%d+$" }, -- /dev/bcache0p1 + { "^/dev/mapper/", ".+$" }, -- /dev/mapper/ + } + 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"):arg({ "-p", "-o", "name,fstype", "-J" }):arg(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:click() end + +function M:scroll() end + +function M:touch() end + +return M diff --git a/.config/yazi/plugins/mount.yazi/sudo.lua b/.config/yazi/plugins/mount.yazi/sudo.lua new file mode 100644 index 0000000..b157e6d --- /dev/null +++ b/.config/yazi/plugins/mount.yazi/sudo.lua @@ -0,0 +1,54 @@ +local M = {} + +--- Verify if `sudo` is already authenticated +--- @return boolean +--- @return Error? +function M.sudo_already() + local status, err = Command("sudo"):arg({ "--validate", "--non-interactive" }):status() + return status and status.success or false, err +end + +--- Run a program with `sudo` privilege +--- @param program string +--- @param args table +--- @return Output? output +--- @return Error? err +function M.run_with_sudo(program, args) + local cmd = Command("sudo") + :arg({ "--stdin", "--user", "#" .. ya.uid(), "--", program }) + :arg(args) + :stdin(Command.PIPED) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + + if M.sudo_already() then + return cmd:output() + end + + local value, event = ya.input { + pos = { "top-center", y = 3, w = 40 }, + title = string.format("Password for `sudo %s`:", program), + obscure = true, + } + if not value or event ~= 1 then + return nil, Err("Sudo password input cancelled") + end + + local child, err = cmd:spawn() + if not child or err then + return nil, err + end + + child:write_all(value .. "\n") + child:flush() + local output, err = child:wait_with_output() + if not output or err then + return nil, err + elseif output.status.success or M.sudo_already() then + return output + else + return nil, Err("Incorrect sudo password") + end +end + +return M diff --git a/.config/yazi/plugins/open-with-cmd.yazi/LICENSE b/.config/yazi/plugins/open-with-cmd.yazi/LICENSE new file mode 100644 index 0000000..0399f1c --- /dev/null +++ b/.config/yazi/plugins/open-with-cmd.yazi/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 Lauri Niskanen + +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/.config/yazi/plugins/open-with-cmd.yazi/README.md b/.config/yazi/plugins/open-with-cmd.yazi/README.md new file mode 100644 index 0000000..ffef1cd --- /dev/null +++ b/.config/yazi/plugins/open-with-cmd.yazi/README.md @@ -0,0 +1,25 @@ +# open-with-cmd.yazi + +This is a Yazi plugin for opening files with a prompted command. + +## Installation + +Install the plugin: + +``` +ya pkg add Ape/open-with-cmd +``` + +Create `~/.config/yazi/keymap.toml` and add: + +``` +[[manager.prepend_keymap]] +on = "o" +run = "plugin open-with-cmd -- block" +desc = "Open with command in the terminal" + +[[manager.prepend_keymap]] +on = "O" +run = "plugin open-with-cmd" +desc = "Open with command" +``` diff --git a/.config/yazi/plugins/open-with-cmd.yazi/main.lua b/.config/yazi/plugins/open-with-cmd.yazi/main.lua new file mode 100644 index 0000000..930512b --- /dev/null +++ b/.config/yazi/plugins/open-with-cmd.yazi/main.lua @@ -0,0 +1,19 @@ +return { + entry = function(_, job) + local block = job.args[1] and job.args[1] == "block" + + local value, event = ya.input({ + title = block and "Open with (block):" or "Open with:", + pos = { "hovered", y = 1, w = 50 }, + }) + + if event == 1 then + local s = ya.target_family() == "windows" and " %*" or ' "$@"' + ya.emit("shell", { + value .. s, + block = block, + orphan = not block, + }) + end + end, +} diff --git a/.config/yazi/plugins/piper.yazi/LICENSE b/.config/yazi/plugins/piper.yazi/LICENSE new file mode 100644 index 0000000..fb5b1d6 --- /dev/null +++ b/.config/yazi/plugins/piper.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/.config/yazi/plugins/piper.yazi/README.md b/.config/yazi/plugins/piper.yazi/README.md new file mode 100644 index 0000000..114c46b --- /dev/null +++ b/.config/yazi/plugins/piper.yazi/README.md @@ -0,0 +1,102 @@ +# piper.yazi + +Pipe any shell command as a previewer. + +## Installation + +```sh +ya pkg add yazi-rs/plugins:piper +``` + +## Usage + +Piper is a general-purpose previewer - you can pass any shell command to `piper` and it will use the command's output as the preview content. + +It accepts a string parameter, which is the shell command to be executed, for example: + +```toml +# ~/.config/yazi/yazi.toml +[[plugin.prepend_previewers]] +url = "*" +run = 'piper -- echo "$1"' +``` + +This will set `piper` as the previewer for all file types and use `$1` (file path) as the preview content. + +## Variables + +Available variables: + +- `$w`: the width of the preview area. +- `$h`: the height of the preview area. +- `$1`: the path to the file being previewed. + +## Examples + +Here are some configuration examples: + +### Preview tarballs with [`tar`](https://man7.org/linux/man-pages/man1/tar.1.html) + +```toml +[[plugin.prepend_previewers]] +url = "*.tar*" +run = 'piper --format=url -- tar tf "$1"' +``` + +In this example, `--format=url` tells `piper` to parse the `tar` output as file URLs, so you'll be able to get a list of files with icons. + +### Preview CSV with [`bat`](https://github.com/sharkdp/bat) + +```toml +[[plugin.prepend_previewers]] +url = "*.csv" +run = 'piper -- bat -p --color=always "$1"' +``` + +Note that certain distributions might use a different name than `bat`, like Debian and Ubuntu use `batcat`, so please adjust accordingly. + +### Preview Markdown with [`glow`](https://github.com/charmbracelet/glow) + +```toml +[[plugin.prepend_previewers]] +url = "*.md" +run = 'piper -- CLICOLOR_FORCE=1 glow -w=$w -s=dark "$1"' +``` + +Note that there's [a bug in Glow v2.0](https://github.com/charmbracelet/glow/issues/440#issuecomment-2307992634) that causes slight color differences between tty and non-tty environments. + +### Preview directory tree with [`eza`](https://github.com/eza-community/eza) + +```toml +[[plugin.prepend_previewers]] +url = "*/" +run = 'piper -- eza -TL=3 --color=always --icons=always --group-directories-first --no-quotes "$1"' +``` + +### Preview the schema of a SQLite database + +```toml +[[plugin.prepend_previewers]] +mime = "application/sqlite3" +run = 'piper -- sqlite3 "$1" ".schema --indent"' +``` + +### Use [`hexyl`](https://github.com/sharkdp/hexyl) as fallback previewer + +Yazi defaults to using [`file -bL "$1"`](https://github.com/sxyazi/yazi/blob/main/yazi-plugin/preset/plugins/file.lua) if there's no matched previewer. + +This example uses `hexyl` as a fallback previewer instead of `file`. + +```toml +[[plugin.append_previewers]] +url = "*" +run = 'piper -- hexyl --border=none --terminal-width=$w "$1"' +``` + +## Related projects + +[`faster-piper.yazi`](https://github.com/alberti42/faster-piper.yazi): a cache-based, scrolling-optimized rewrite compatible with `piper.yazi`. + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/.config/yazi/plugins/piper.yazi/main.lua b/.config/yazi/plugins/piper.yazi/main.lua new file mode 100644 index 0000000..4c2a51c --- /dev/null +++ b/.config/yazi/plugins/piper.yazi/main.lua @@ -0,0 +1,71 @@ +--- @since 26.1.22 + +local M = {} + +local function fail(job, s) ya.preview_widget(job, ui.Text.parse(s):area(job.area):wrap(ui.Wrap.YES)) end + +function M:peek(job) + local child, err = Command("sh") + :arg({ "-c", job.args[1], "sh", tostring(job.file.path) }) + :env("w", job.area.w) + :env("h", job.area.h) + :env("CYGWIN", "noupcaseenv") + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :spawn() + + if not child then + return fail(job, "sh: " .. err) + end + + local limit = job.area.h + local i, outs, errs = 0, {}, {} + repeat + local next, event = child:read_line() + if event == 1 then + errs[#errs + 1] = next + elseif event ~= 0 then + break + end + + i = i + 1 + if i > job.skip then + outs[#outs + 1] = next + end + until i >= job.skip + limit + + child:start_kill() + if #errs > 0 then + fail(job, table.concat(errs, "")) + elseif job.skip > 0 and i < job.skip + limit then + ya.emit("peek", { math.max(0, i - limit), only_if = job.file.url, upper_bound = true }) + else + ya.preview_widget(job, M.format(job, outs)) + end +end + +function M:seek(job) require("code"):seek(job) end + +function M.format(job, lines) + local format = job.args.format + if format ~= "url" then + local s = table.concat(lines, ""):gsub("\t", string.rep(" ", rt.preview.tab_size)) + return ui.Text.parse(s):area(job.area) + end + + for i = 1, #lines do + lines[i] = lines[i]:gsub("[\r\n]+$", "") + + local icon = File({ + url = Url(lines[i]), + cha = Cha { mode = tonumber(lines[i]:sub(-1) == "/" and "40700" or "100644", 8) }, + }):icon() + + if icon then + lines[i] = ui.Line { ui.Span(" " .. icon.text .. " "):style(icon.style), lines[i] } + end + end + return ui.Text(lines):area(job.area) +end + +return M diff --git a/.config/yazi/plugins/relative-motions.yazi/LICENSE b/.config/yazi/plugins/relative-motions.yazi/LICENSE new file mode 100644 index 0000000..f8ead01 --- /dev/null +++ b/.config/yazi/plugins/relative-motions.yazi/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 dedukun + +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/.config/yazi/plugins/relative-motions.yazi/README.md b/.config/yazi/plugins/relative-motions.yazi/README.md new file mode 100644 index 0000000..106d575 --- /dev/null +++ b/.config/yazi/plugins/relative-motions.yazi/README.md @@ -0,0 +1,145 @@ +# relative-motions.yazi + +A [Yazi](https://github.com/sxyazi/yazi) plugin based about vim motions. + + + +## Requirements + +- [Yazi](https://github.com/sxyazi/yazi) v25.5.28+ + +## Installation + +```sh +ya pkg add dedukun/relative-motions +``` + +## Configuration + +If you want to use the numbers directly to start a motion add this to your `keymap.toml`: + +
+ +```toml +[[mgr.prepend_keymap]] +on = [ "1" ] +run = "plugin relative-motions 1" +desc = "Move in relative steps" + +[[mgr.prepend_keymap]] +on = [ "2" ] +run = "plugin relative-motions 2" +desc = "Move in relative steps" + +[[mgr.prepend_keymap]] +on = [ "3" ] +run = "plugin relative-motions 3" +desc = "Move in relative steps" + +[[mgr.prepend_keymap]] +on = [ "4" ] +run = "plugin relative-motions 4" +desc = "Move in relative steps" + +[[mgr.prepend_keymap]] +on = [ "5" ] +run = "plugin relative-motions 5" +desc = "Move in relative steps" + +[[mgr.prepend_keymap]] +on = [ "6" ] +run = "plugin relative-motions 6" +desc = "Move in relative steps" + +[[mgr.prepend_keymap]] +on = [ "7" ] +run = "plugin relative-motions 7" +desc = "Move in relative steps" + +[[mgr.prepend_keymap]] +on = [ "8" ] +run = "plugin relative-motions 8" +desc = "Move in relative steps" + +[[mgr.prepend_keymap]] +on = [ "9" ] +run = "plugin relative-motions 9" +desc = "Move in relative steps" +``` + +
+ +Alternatively you can use a key to trigger a new motion without any initial value, for that add the following in `keymap.toml`: + +```toml +[[mgr.prepend_keymap]] +on = [ "m" ] +run = "plugin relative-motions" +desc = "Trigger a new relative motion" +``` + +--- + +Additionally there are a couple of initial configurations that can be given to the plugin's `setup` function: + +| Configuration | Values | Default | Description | +| -------------- | ----------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `show_numbers` | `relative`, `absolute`, `relative_absolute` or `none` | `none` | Shows relative or absolute numbers before the file icon | +| `show_motion` | `true` or `false` | `false` | Shows current motion in Status bar | +| `only_motions` | `true` or `false` | `false` | If true, only the motion movements will be enabled, i.e., the commands for delete, cut, yank and visual selection will be disabled | +| `enter_mode` | `cache`, `first` or `cache_or_first` | `cache_or_first` | The method to enter folders | + +If you want, for example, to enable relative numbers as well as to show the motion in the status bar, +add the following to Yazi's `init.lua`, i.e. `~/.config/yazi/init.lua`: + +```lua +-- ~/.config/yazi/init.lua +require("relative-motions"):setup({ show_numbers="relative", show_motion = true, enter_mode ="first" }) +``` + +> [!NOTE] +> The `show_numbers` and `show_motion` functionalities overwrite [`Current:redraw`](https://github.com/sxyazi/yazi/blob/e3c91115a2c096724303a0b364e7625691e4beba/yazi-plugin/preset/components/current.lua#L28) +> and [`Status:children_redraw`](https://github.com/sxyazi/yazi/blob/e3c91115a2c096724303a0b364e7625691e4beba/yazi-plugin/preset/components/status.lua#L177) respectively. +> If you have custom implementations for any of this functions +> you can add the provided `Entity:number` and `Status:motion` to your implementations, just check [here](https://github.com/dedukun/relative-motions.yazi/blob/main/init.lua#L126) how we are doing things. + +## Usage + +This plugin adds the some basic vim motions like `3k`, `12j`, `10gg`, etc. +The following table show all the available motions: + +| Command | Description | +| -------------- | --------------------- | +| `j`/`` | Move `n` lines down | +| `k`/`` | Move `n` lines up | +| `h`/`` | Move `n` folders back | +| `l`/`` | Enter `n` folders | +| `gj`/`g` | Go `n` lines down | +| `gk`/`g` | Go `n` lines up | +| `gg` | Go to line | + +Furthermore, the following operations were also added: + +| Command | Description | +| ------- | ------------- | +| `v` | visual select | +| `y` | Yank | +| `x` | Cut | +| `d` | Delete motion | + +This however must be followed by a direction, which can be `j`/``, `k`/`` or repeating the command key, +which will operate from the cursor down, e.g. `2yy` will copy two files. + +Finally, we also support some tab operations: + +| Command | Description | +| ------- | ------------------------------------ | +| `t` | create `n` tabs | +| `H` | Move `n` tabs left | +| `L` | Move `n` tabs right | +| `gt` | Go to the `n` tab | +| `w` | Close tab `n` | +| `W` | Close `n` tabs right | +| `<` | Swap current tab with `n` tabs left | +| `>` | Swap current tab with `n` tabs right | +| `~` | Swap current tab with tab `n` | diff --git a/.config/yazi/plugins/relative-motions.yazi/main.lua b/.config/yazi/plugins/relative-motions.yazi/main.lua new file mode 100644 index 0000000..356b132 --- /dev/null +++ b/.config/yazi/plugins/relative-motions.yazi/main.lua @@ -0,0 +1,406 @@ +--- @since 25.5.28 +-- stylua: ignore +local MOTIONS_AND_OP_KEYS = { + { on = "0" }, { on = "1" }, { on = "2" }, { on = "3" }, { on = "4" }, + { on = "5" }, { on = "6" }, { on = "7" }, { on = "8" }, { on = "9" }, + -- commands + { on = "d" }, { on = "v" }, { on = "y" }, { on = "x" }, + -- tab commands + { on = "t" }, { on = "L" }, { on = "H" }, { on = "w" }, + { on = "W" }, { on = "<" }, { on = ">" }, { on = "~" }, + -- movement + { on = "g" }, { on = "j" }, { on = "k" }, { on = "h" }, { on = "l" }, { on = "" }, { on = "" }, { on = "" }, { on = "" } +} + +-- stylua: ignore +local MOTION_KEYS = { + { on = "0" }, { on = "1" }, { on = "2" }, { on = "3" }, { on = "4" }, + { on = "5" }, { on = "6" }, { on = "7" }, { on = "8" }, { on = "9" }, + -- movement + { on = "g" }, { on = "j" }, { on = "k" }, { on = "h" }, { on = "l" }, { on = "" }, { on = "" }, { on = "" }, { on = "" } +} + +-- stylua: ignore +local DIRECTION_KEYS = { + { on = "j" }, { on = "k" }, { on = "" }, { on = "" }, + -- tab movement + { on = "t" } +} + +local SHOW_NUMBERS_ABSOLUTE = 0 +local SHOW_NUMBERS_RELATIVE = 1 +local SHOW_NUMBERS_RELATIVE_ABSOLUTE = 2 + +local ENTER_MODE_FIRST = 0 +local ENTER_MODE_CACHE = 1 +local ENTER_MODE_CACHE_OR_FIRST = 2 + +----------------------------------------------- +----------------- R E N D E R ----------------- +----------------------------------------------- + +local render_motion_setup = ya.sync(function(_) + if ui.render then + ui.render() + else + ya.render() + end + + Status.motion = function() return ui.Span("") end + + Status.children_redraw = function(self, side) + local lines = {} + if side == self.RIGHT then + lines[1] = self:motion(self) + end + for _, c in ipairs(side == self.RIGHT and self._right or self._left) do + lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self) + end + return ui.Line(lines) + end + + -- TODO: check why it doesn't work line this + -- Status:children_add(function() return ui.Span("") end, 1000, Status.RIGHT) +end) + +local render_motion = ya.sync(function(_, motion_num, motion_cmd) + if ui.render then + ui.render() + else + ya.render() + end + + Status.motion = function(self) + if not motion_num then + return ui.Span("") + end + + local style = self:style() + + local motion_span + if not motion_cmd then + motion_span = ui.Span(string.format(" %3d ", motion_num)) + else + motion_span = ui.Span(string.format(" %3d%s ", motion_num, motion_cmd)) + end + + local status_config = th.status + local separator_open = status_config.sep_right.open + local separator_close = status_config.sep_right.close + + -- TODO: REMOVE THIS IN NEXT RELEASE + local bg_style + if type(style.main.bg) == "function" then + bg_style = style.main:bg() + else + bg_style = style.main.bg + end + + return ui.Line { + ui.Span(separator_open):fg(bg_style), + motion_span:style(style.main), + ui.Span(separator_close):fg(bg_style), + ui.Span(" "), + } + end +end) + +local render_numbers = ya.sync(function(_, mode) + if ui.render then + ui.render() + else + ya.render() + end + + Entity.number = function(_, index, total, file, hovered) + local idx + if mode == SHOW_NUMBERS_RELATIVE then + idx = math.abs(hovered - index) + elseif mode == SHOW_NUMBERS_ABSOLUTE then + idx = file.idx + else -- SHOW_NUMBERS_RELATIVE_ABSOLUTE + if hovered == index then + idx = file.idx + else + idx = math.abs(hovered - index) + end + end + + local num_format = "%" .. #tostring(total) .. "d" + + -- emulate vim's hovered offset + if hovered == index then + return ui.Span(string.format(num_format .. " ", idx)) + else + return ui.Span(string.format(" " .. num_format, idx)) + end + end + + Current.redraw = function(self) + local files = self._folder.window + if #files == 0 then + return self:empty() + end + + local hovered_index + for i, f in ipairs(files) do + if f.is_hovered then + hovered_index = i + break + end + end + + local entities, linemodes = {}, {} + for i, f in ipairs(files) do + linemodes[#linemodes + 1] = Linemode:new(f):redraw() + + local entity = Entity:new(f) + entities[#entities + 1] = ui.Line({ Entity:number(i, #self._folder.files, f, hovered_index), entity:redraw() }) + :style(entity:style()) + end + + return { + ui.List(entities):area(self._area), + ui.Text(linemodes):area(self._area):align(ui.Align.RIGHT), + } + end +end) + +local function render_clear() render_motion() end + +----------------------------------------------- +--------- C O M M A N D P A R S E R --------- +----------------------------------------------- + +local get_keys = ya.sync(function(state) return state._only_motions and MOTION_KEYS or MOTIONS_AND_OP_KEYS end) + +local function normal_direction(dir) + if dir == "" then + return "j" + elseif dir == "" then + return "k" + elseif dir == "" then + return "h" + elseif dir == "" then + return "l" + end + return dir +end + +local function get_cmd(first_char, keys) + local last_key + local lines = first_char or "" + + while true do + render_motion(tonumber(lines)) + local key = ya.which { cands = keys, silent = true } + if not key then + return nil, nil, nil + end + + last_key = keys[key].on + if not tonumber(last_key) then + last_key = normal_direction(last_key) + break + end + + lines = lines .. last_key + end + + render_motion(tonumber(lines), last_key) + + -- command direction + local direction + if last_key == "g" or last_key == "v" or last_key == "d" or last_key == "y" or last_key == "x" then + DIRECTION_KEYS[#DIRECTION_KEYS + 1] = { + on = last_key, + } + local direction_key = ya.which { cands = DIRECTION_KEYS, silent = true } + if not direction_key then + return nil, nil, nil + end + + direction = DIRECTION_KEYS[direction_key].on + direction = normal_direction(direction) + end + + return tonumber(lines), last_key, direction +end + +local function is_tab_command(command) + local tab_commands = { "t", "L", "H", "w", "W", "<", ">", "~" } + for _, cmd in ipairs(tab_commands) do + if command == cmd then + return true + end + end + return false +end + +local get_active_tab = ya.sync(function(_) return cx.tabs.idx end) + +local get_cache_or_first_dir = ya.sync(function(state) + if state._enter_mode == ENTER_MODE_CACHE then + return nil + elseif state._enter_mode == ENTER_MODE_CACHE_OR_FIRST then + local hovered_file = cx.active.current.hovered + + if hovered_file ~= nil and hovered_file.cha.is_dir then + return cx.active.current.cursor + end + end + + local files = cx.active.current.files + local index = 1 + + for i = 1, #files do + if files[i].cha.is_dir then + index = i + break + end + end + + return index - 1 +end) +----------------------------------------------- +---------- E N T R Y / S E T U P ---------- +----------------------------------------------- + +return { + entry = function(_, job) + local initial_value + + local args = job.args + -- this is checking if the argument is a valid number + if #args > 0 then + initial_value = tostring(tonumber(args[1])) + if initial_value == "nil" then + return + end + end + + local lines, cmd, direction = get_cmd(initial_value, get_keys()) + if not lines or not cmd then + -- command was cancelled + render_clear() + return + end + + if cmd == "g" then + if direction == "g" then + ya.mgr_emit("arrow", { "top" }) + ya.mgr_emit("arrow", { lines - 1 }) + render_clear() + return + elseif direction == "j" then + cmd = "j" + elseif direction == "k" then + cmd = "k" + elseif direction == "t" then + ya.mgr_emit("tab_switch", { lines - 1 }) + render_clear() + return + else + -- no valid direction + render_clear() + return + end + end + + if cmd == "j" then + ya.mgr_emit("arrow", { lines }) + elseif cmd == "k" then + ya.mgr_emit("arrow", { -lines }) + elseif cmd == "h" then + for _ = 1, lines do + ya.mgr_emit("leave", {}) + end + elseif cmd == "l" then + for _ = 1, lines do + ya.mgr_emit("enter", {}) + local file_idx = get_cache_or_first_dir() + if file_idx then + ya.mgr_emit("arrow", { "top" }) + ya.mgr_emit("arrow", { file_idx }) + end + end + elseif is_tab_command(cmd) then + if cmd == "t" then + for _ = 1, lines do + ya.mgr_emit("tab_create", {}) + end + elseif cmd == "H" then + ya.mgr_emit("tab_switch", { -lines, relative = true }) + elseif cmd == "L" then + ya.mgr_emit("tab_switch", { lines, relative = true }) + elseif cmd == "w" then + ya.mgr_emit("tab_close", { lines - 1 }) + elseif cmd == "W" then + local curr_tab = get_active_tab() + local del_tab = curr_tab + lines - 1 + for _ = curr_tab, del_tab do + ya.mgr_emit("tab_close", { curr_tab - 1 }) + end + ya.mgr_emit("tab_switch", { curr_tab - 1 }) + elseif cmd == "<" then + ya.mgr_emit("tab_swap", { -lines }) + elseif cmd == ">" then + ya.mgr_emit("tab_swap", { lines }) + elseif cmd == "~" then + local jump = lines - get_active_tab() + ya.mgr_emit("tab_swap", { jump }) + end + else + ya.mgr_emit("visual_mode", {}) + -- invert direction when user specifies it + if direction == "k" then + ya.mgr_emit("arrow", { -lines }) + elseif direction == "j" then + ya.mgr_emit("arrow", { lines }) + else + ya.mgr_emit("arrow", { lines - 1 }) + end + ya.mgr_emit("escape", {}) + + if cmd == "d" then + ya.mgr_emit("remove", {}) + elseif cmd == "y" then + ya.mgr_emit("yank", {}) + elseif cmd == "x" then + ya.mgr_emit("yank", { cut = true }) + end + end + + render_clear() + end, + setup = function(state, args) + if not args then + return + end + + -- initialize state variables + state._only_motions = args["only_motions"] or false + + if args["show_motion"] then + render_motion_setup() + end + + if args["enter_mode"] == "cache" then + state._enter_mode = ENTER_MODE_CACHE + elseif args["enter_mode"] == "first" then + state._enter_mode = ENTER_MODE_FIRST + elseif args["enter_mode"] == "cache_or_first" then + state._enter_mode = ENTER_MODE_CACHE_OR_FIRST + else + state._enter_mode = ENTER_MODE_CACHE_OR_FIRST + end + + if args["show_numbers"] == "absolute" then + render_numbers(SHOW_NUMBERS_ABSOLUTE) + elseif args["show_numbers"] == "relative" then + render_numbers(SHOW_NUMBERS_RELATIVE) + elseif args["show_numbers"] == "relative_absolute" then + render_numbers(SHOW_NUMBERS_RELATIVE_ABSOLUTE) + end + end, +} diff --git a/.config/yazi/plugins/smart-enter.yazi/LICENSE b/.config/yazi/plugins/smart-enter.yazi/LICENSE new file mode 100644 index 0000000..fb5b1d6 --- /dev/null +++ b/.config/yazi/plugins/smart-enter.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/.config/yazi/plugins/smart-enter.yazi/README.md b/.config/yazi/plugins/smart-enter.yazi/README.md new file mode 100644 index 0000000..9c19bc7 --- /dev/null +++ b/.config/yazi/plugins/smart-enter.yazi/README.md @@ -0,0 +1,40 @@ +# smart-enter.yazi + +[`Open`][open] files or [`enter`][enter] directories all in one key! + +## Installation + +```sh +ya pkg add yazi-rs/plugins:smart-enter +``` + +## Usage + +Bind your l key to the plugin, in your `~/.config/yazi/keymap.toml`: + +```toml +[[mgr.prepend_keymap]] +on = "l" +run = "plugin smart-enter" +desc = "Enter the child directory, or open the file" +``` + +## Advanced + +By default, `--hovered` is passed to the [`open`][open] action, make the behavior consistent with [`enter`][enter] avoiding accidental triggers, +which means both will only target the currently hovered file. + +If you still want `open` to target multiple selected files, add this to your `~/.config/yazi/init.lua`: + +```lua +require("smart-enter"):setup { + open_multi = true, +} +``` + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. + +[open]: https://yazi-rs.github.io/docs/configuration/keymap/#mgr.open +[enter]: https://yazi-rs.github.io/docs/configuration/keymap/#mgr.enter diff --git a/.config/yazi/plugins/smart-enter.yazi/main.lua b/.config/yazi/plugins/smart-enter.yazi/main.lua new file mode 100644 index 0000000..e9e2ec6 --- /dev/null +++ b/.config/yazi/plugins/smart-enter.yazi/main.lua @@ -0,0 +1,11 @@ +--- @since 25.5.31 +--- @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.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/.config/yazi/plugins/zoom.yazi/LICENSE b/.config/yazi/plugins/zoom.yazi/LICENSE new file mode 100644 index 0000000..fb5b1d6 --- /dev/null +++ b/.config/yazi/plugins/zoom.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/.config/yazi/plugins/zoom.yazi/README.md b/.config/yazi/plugins/zoom.yazi/README.md new file mode 100644 index 0000000..b0a3149 --- /dev/null +++ b/.config/yazi/plugins/zoom.yazi/README.md @@ -0,0 +1,55 @@ +# zoom.yazi + +Enlarge or shrink the preview image of a file, which is useful for magnifying small files for viewing. + +Supported formats: + +- Images - requires [ImageMagick](https://imagemagick.org/) (>= 7.1.1) + +Note that, the maximum size of enlarged images is limited by the [`max_width`][max_width] and [`max_height`][max_height] configuration options, so you may need to increase them as needed. + +https://github.com/user-attachments/assets/b28912b1-da63-43d3-a21f-b9e6767ed4a9 + +[max_width]: https://yazi-rs.github.io/docs/configuration/yazi#preview.max_width +[max_height]: https://yazi-rs.github.io/docs/configuration/yazi#preview.max_height + +## Installation + +```sh +ya pkg add yazi-rs/plugins:zoom +``` + +## Usage + +```toml +# keymap.toml +[[mgr.prepend_keymap]] +on = "+" +run = "plugin zoom 1" +desc = "Zoom in hovered file" + +[[mgr.prepend_keymap]] +on = "-" +run = "plugin zoom -1" +desc = "Zoom out hovered file" +``` + +Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other actions/plugins. + +## Advanced + +If you want to apply a default zoom parameter to image previews, you can specify it while setting this plugin up as a custom previewer, for example: + +```toml +[[plugin.prepend_previewers]] +mime = "image/{jpeg,png,webp}" +run = "zoom 5" +``` + +## TODO + +- [ ] Support more file types (e.g., videos, PDFs), PRs welcome! + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/.config/yazi/plugins/zoom.yazi/main.lua b/.config/yazi/plugins/zoom.yazi/main.lua new file mode 100644 index 0000000..cb0daf0 --- /dev/null +++ b/.config/yazi/plugins/zoom.yazi/main.lua @@ -0,0 +1,119 @@ +--- @since 26.1.22 + +local get = ya.sync(function(st, url) return st.last == url and st.level end) + +local save = ya.sync(function(st, url, new) + local h = cx.active.current.hovered + if h and h.url == url then + st.last, st.level = url, new + return true + end +end) + +local lock = ya.sync(function(st, url, old, new) + if st.last == url and st.level == old then + st.level = new + return true + end +end) + +local move = ya.sync(function(st) + local h = cx.active.current.hovered + if not h then + return + end + + if st.last ~= h.url then + st.last, st.level = Url(h.url), 0 + end + + return { url = h.url, level = st.level } +end) + +local function end_(job, err) + if not job.old_level then + ya.preview_widget(job, err and ui.Text(err):area(job.area):wrap(ui.Wrap.YES)) + elseif err then + ya.notify { title = "Zoom", content = tostring(err), timeout = 5, level = "error" } + end +end + +local function canvas(area) + local cw, ch = rt.term.cell_size() + if not cw then + return rt.preview.max_width, rt.preview.max_height + end + + return math.min(rt.preview.max_width, math.floor(area.w * cw)), + math.min(rt.preview.max_height, math.floor(area.h * ch)) +end + +local function peek(_, job) + local url = job.file.url + local info, err = ya.image_info(url) + if not info then + return end_(job, Err("Failed to get image info: %s", err)) + end + + local level = ya.clamp(-10, job.new_level or get(Url(url)) or tonumber(job.args[1]) or 0, 10) + local sync = function() + if job.old_level then + return lock(url, job.old_level, level) + else + return save(url, level) + end + end + + local max_w, max_h = canvas(job.area) + local min_w, min_h = math.min(max_w, info.w), math.min(max_h, info.h) + local new_w = min_w + math.floor(min_w * level * 0.1) + local new_h = min_h + math.floor(min_h * level * 0.1) + if new_w > max_w or new_h > max_h then + if job.old_level then + return sync() -- Image larger than available preview area after zooming + else + new_w, new_h = max_w, max_h -- Run as a previewer, render the image anyway + end + end + + local tmp = os.tmpname() + -- stylua: ignore + local output, err = Command("magick"):arg { + tostring(job.file.path), + "-auto-orient", "-strip", + "-sample", string.format("%dx%d", new_w, new_h), + "-quality", rt.preview.image_quality, + string.format("JPG:%s", tmp), + }:output() + + if not output then + end_(job, Err("Failed to start `magick`, error: %s", err)) + elseif not output.status.success then + end_(job, Err("`magick` exited with error code %s: %s", output.status.code, output.stderr)) + elseif sync() then + ya.image_show(Url(tmp), job.area) + end + end_(job) +end + +local function entry(self, job) + local st = move() + if not st then + return + end + + local motion = tonumber(job.args[1]) or 0 + local new = ya.clamp(-10, st.level + motion, 10) + if new ~= st.level then + peek(self, { + area = ui.area("preview"), + args = {}, + file = File { url = st.url, cha = Cha { mode = tonumber("100644", 8) } }, + skip = 0, + new_level = new, + old_level = st.level, + }) + end +end + +return { peek = peek, entry = entry } diff --git a/.config/yazi/theme.toml b/.config/yazi/theme.toml new file mode 100644 index 0000000..f5379bf --- /dev/null +++ b/.config/yazi/theme.toml @@ -0,0 +1,3 @@ +[flavor] +# dark = "everforest-medium" +dark = "gruvbox-material" diff --git a/.config/yazi/yazi.toml b/.config/yazi/yazi.toml new file mode 100644 index 0000000..21409dd --- /dev/null +++ b/.config/yazi/yazi.toml @@ -0,0 +1,37 @@ +[[plugin.prepend_fetchers]] +# id = "git" # Remove if Yazi > v26.1.22 +url = "*" +run = "git" +group = "git" + +[[plugin.prepend_fetchers]] +# id = "git" # Remove if Yazi > v26.1.22 +url = "*/" +run = "git" +group = "git" + +# [opener] +# edit = [ +# { run = 'nvim "$@"', desc = "Open in Neovim", block = true, for = "unix" }, +# ] + +# [[plugin.prepend_previewers]] +# url = "*" +# run = 'piper -- echo "$1"' + +[[plugin.prepend_previewers]] +url = "*.md" +run = 'piper -- CLICOLOR_FORCE=1 glow -w=$w -s=dark "$1"' + +[[plugin.prepend_previewers]] +url = "*/" +run = 'piper -- eza -TL=3 --color=always --icons=always --group-directories-first --no-quotes "$1"' + +[[plugin.prepend_previewers]] +mime = "application/sqlite3" +run = 'piper -- sqlite3 "$1" ".schema --indent"' + +[[plugin.append_previewers]] +url = "*" +run = 'piper -- hexyl --border=none --terminal-width=$w "$1"' + diff --git a/.dotfiles.d/init/ubuntu/init-cargo-tools.sh b/.dotfiles.d/init/ubuntu/init-cargo-tools.sh new file mode 100644 index 0000000..910ed48 --- /dev/null +++ b/.dotfiles.d/init/ubuntu/init-cargo-tools.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -euo pipefail + +echo "[1/4] Updating apt and installing prerequisites (curl, build-essential)..." +sudo apt update -qq +sudo apt install -y curl build-essential imagemagick + +sudo ln -s /usr/bin/convert /usr/bin/magick + +echo "[2/4] Downloading and installing rustup (latest rustc/cargo)..." +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +echo "[3/4] Sourcing cargo environment..." +source "$HOME/.cargo/env" + +echo " -> Installed: $(rustc --version)" +echo " -> Installed: $(cargo --version)" + +echo "[4/4] Installing tools..." +cargo install fsel +cargo install yazi +cargo install glow +cargo install resvg + +echo "Done!" diff --git a/.dotfiles.d/init/init-nvim.sh b/.dotfiles.d/init/ubuntu/init-nvim.sh similarity index 100% rename from .dotfiles.d/init/init-nvim.sh rename to .dotfiles.d/init/ubuntu/init-nvim.sh diff --git a/.dotfiles.d/init/init-shell.sh b/.dotfiles.d/init/ubuntu/init-shell.sh similarity index 90% rename from .dotfiles.d/init/init-shell.sh rename to .dotfiles.d/init/ubuntu/init-shell.sh index 6da362d..f8dc468 100755 --- a/.dotfiles.d/init/init-shell.sh +++ b/.dotfiles.d/init/ubuntu/init-shell.sh @@ -2,13 +2,12 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" -# ubuntu sudo apt update -y sudo apt install -y zsh eza unzip locales sudo chsh -s $(which zsh) $USER -source "${SCRIPT_DIR}/ensure-locale.sh" +source "${SCRIPT_DIR}/../ensure-locale.sh" cd ~ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf