diff --git a/.dotter/global.toml b/.dotter/global.toml index 894e34a3..039bed56 100644 --- a/.dotter/global.toml +++ b/.dotter/global.toml @@ -192,11 +192,10 @@ depends = [ "tmux", "zsh" ] "config/gtk-4.0/" = "~/.config/gtk-4.0/" [codex.files] -# "config/AGENTS.md" = "~/.codex/AGENTS.md" +"config/AGENTS.md" = "~/.codex/AGENTS.md" [claude.files] -# "config/AGENTS.md" = "~/.claude/AGENTS.md" +"config/AGENTS-CLAUDE.md" = "~/.claude/AGENTS.md" [opencode.files] "config/opencode/" = "~/.config/opencode/" -"config/AGENTS.md" = "~/.config/opencode/AGENTS.md" diff --git a/config/AGENTS-CLAUDE.md b/config/AGENTS-CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /dev/null +++ b/config/AGENTS-CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/config/Vencord/settings/settings.json b/config/Vencord/settings/settings.json index 957b6b99..2244c75e 100644 --- a/config/Vencord/settings/settings.json +++ b/config/Vencord/settings/settings.json @@ -891,7 +891,7 @@ "authenticated": true, "url": "https://api.vencord.dev/", "settingsSync": true, - "settingsSyncVersion": 1770991316025 + "settingsSyncVersion": 1773063985102 }, "enabledThemes": [], "eagerPatches": false, diff --git a/config/ashell/config.toml b/config/ashell/config.toml index 7dc499d4..9d81a18f 100644 --- a/config/ashell/config.toml +++ b/config/ashell/config.toml @@ -2,6 +2,7 @@ log_level = "warn" outputs = "All" position = "Top" app_launcher_cmd = "fuzzel" +enable_esc_key = true [modules] left = [ [ "Workspaces" ] ] @@ -14,7 +15,7 @@ update_cmd = 'alacritty -e bash -c "paru; echo Done - Press enter to exit; read" [workspaces] visibility_mode = "All" -# enable_workspace_filling = true +# enable_workspace_filling = false # disable_special_workspaces = true [clock] @@ -34,6 +35,7 @@ reboot_cmd = "loginctl reboot" logout_cmd = "logout" remove_airplane_btn = true remove_idle_btn = true +peripheral_indicators = {Specific = [ "Gamepad", "Keyboard" ]} [system_info] indicators = [ "Cpu", "Memory", "Temperature" ] diff --git a/config/bat/config b/config/bat/config index c8bfa74e..65222cb5 100644 --- a/config/bat/config +++ b/config/bat/config @@ -4,7 +4,7 @@ # Specify desired highlighting theme (e.g. "TwoDark"). Run `bat --list-themes` # for a list of all available themes ---theme="Rosé Pine" +--theme="rose-pine" # Enable this to use italic text on the terminal. This is not supported on all # terminal emulators (like tmux, by default): diff --git a/config/fish/completions/tree-sitter.fish b/config/fish/completions/tree-sitter.fish new file mode 100644 index 00000000..18d75502 --- /dev/null +++ b/config/fish/completions/tree-sitter.fish @@ -0,0 +1,158 @@ +# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. +function __fish_tree_sitter_global_optspecs + string join \n h/help V/version +end + +function __fish_tree_sitter_needs_command + # Figure out if the current invocation already has a command. + set -l cmd (commandline -opc) + set -e cmd[1] + argparse -s (__fish_tree_sitter_global_optspecs) -- $cmd 2>/dev/null + or return + if set -q argv[1] + # Also print the command, so this can be used to figure out what it is. + echo $argv[1] + return 1 + end + return 0 +end + +function __fish_tree_sitter_using_subcommand + set -l cmd (__fish_tree_sitter_needs_command) + test -z "$cmd" + and return 1 + contains -- $cmd[1] $argv +end + +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -s V -l version -d 'Print version' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "init-config" -d 'Generate a default config file' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "init" -d 'Initialize a grammar repository' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "generate" -d 'Generate a parser' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "build" -d 'Compile a parser' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "parse" -d 'Parse files' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "test" -d 'Run a parser\'s tests' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "version" -d 'Increment the version of a grammar' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "fuzz" -d 'Fuzz a parser' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "query" -d 'Search files using a syntax tree query' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "highlight" -d 'Highlight a file' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "tags" -d 'Generate a list of tags' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "playground" -d 'Start local playground for a parser in the browser' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "dump-languages" -d 'Print info about all known language parsers' +complete -c tree-sitter -n "__fish_tree_sitter_needs_command" -f -a "complete" -d 'Generate shell completions' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand init-config" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand init" -s u -l update -d 'Update outdated files' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand init" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -l abi -d 'Select the language ABI version to generate (default 15). Use --abi=latest to generate the newest supported version (15).' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -l libdir -d 'The path to the directory containing the parser library' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -s o -l output -d 'The path to output the generated source files' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -l report-states-for-rule -d 'Produce a report of the states for the given rule, use `-` to report every rule' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -l js-runtime -d 'The name or path of the JavaScript runtime to use for generating parsers' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -s l -l log -d 'Show debug log during generation' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -l no-bindings -d 'Deprecated (no-op)' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -s b -l build -d 'Compile all defined languages in the current dir' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -s 0 -l debug-build -d 'Compile a parser in debug mode' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -l json -d 'Report conflicts in a JSON format' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand generate" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand build" -s o -l output -d 'The path to output the compiled file' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand build" -s w -l wasm -d 'Build a WASM module instead of a dynamic library' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand build" -s d -l docker -d 'Run emscripten via docker even if it is installed locally (only if building a WASM module with --wasm)' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand build" -l reuse-allocator -d 'Make the parser reuse the same allocator as the library' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand build" -s 0 -l debug -d 'Compile a parser in debug mode' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand build" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -l paths -d 'The path to a file with paths to source file(s)' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -l scope -d 'Select a language by the scope instead of a file extension' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s d -l debug -d 'Show parsing debug log' -r -f -a "quiet\t'' +normal\t'' +pretty\t''" +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -l timeout -d 'Interrupt the parsing process by timeout (µs)' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -l edits -d 'Apply edits in the format: \\"row,col|position delcount insert_text\\", can be supplied multiple times' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -l encoding -d 'The encoding of the input files' -r -f -a "utf8\t'' +utf16-le\t'' +utf16-be\t''" +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -l config-path -d 'The path to an alternative config.json file' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s n -l test-number -d 'Parse the contents of a specific test' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s 0 -l debug-build -d 'Compile a parser in debug mode' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s D -l debug-graph -d 'Produce the log.html file with debug graphs' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -l wasm -d 'Compile parsers to wasm instead of native dynamic libraries' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -l dot -d 'Output the parse data with graphviz dot' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s x -l xml -d 'Output the parse data in XML format' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s c -l cst -d 'Output the parse data in a pretty-printed CST format' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s s -l stat -d 'Show parsing statistic' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s t -l time -d 'Measure execution time' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s q -l quiet -d 'Suppress main output' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -l open-log -d 'Open `log.html` in the default browser, if `--debug-graph` is supplied' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s j -l json -d 'Output parsing results in a JSON format' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s r -l rebuild -d 'Force rebuild the parser' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -l no-ranges -d 'Omit ranges in the output' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand parse" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -s i -l include -d 'Only run corpus test cases whose name matches the given regex' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -s e -l exclude -d 'Only run corpus test cases whose name does not match the given regex' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -l file-name -d 'Only run corpus test cases from from a given filename' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -l config-path -d 'The path to an alternative config.json file' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -l stat -d 'Show parsing statistics' -r -f -a "all\t'' +outliers-and-total\t'' +total-only\t''" +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -s u -l update -d 'Update all syntax trees in corpus files with current parser output' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -s d -l debug -d 'Show parsing debug log' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -s 0 -l debug-build -d 'Compile a parser in debug mode' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -s D -l debug-graph -d 'Produce the log.html file with debug graphs' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -l wasm -d 'Compile parsers to wasm instead of native dynamic libraries' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -l open-log -d 'Open `log.html` in the default browser, if `--debug-graph` is supplied' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -l show-fields -d 'Force showing fields in test diffs' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -s r -l rebuild -d 'Force rebuild the parser' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -l overview-only -d 'Show only the pass-fail overview tree' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand test" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand version" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand fuzz" -s s -l skip -d 'List of test names to skip' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand fuzz" -l subdir -d 'Subdirectory to the language' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand fuzz" -l edits -d 'Maximum number of edits to perform per fuzz test' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand fuzz" -l iterations -d 'Number of fuzzing iterations to run per test' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand fuzz" -s i -l include -d 'Only fuzz corpus test cases whose name matches the given regex' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand fuzz" -s e -l exclude -d 'Only fuzz corpus test cases whose name does not match the given regex' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand fuzz" -l log-graphs -d 'Enable logging of graphs and input' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand fuzz" -s l -l log -d 'Enable parser logging' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand fuzz" -s r -l rebuild -d 'Force rebuild the parser' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand fuzz" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -l paths -d 'The path to a file with paths to source file(s)' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -l byte-range -d 'The range of byte offsets in which the query will be executed' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -l row-range -d 'The range of rows in which the query will be executed' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -l scope -d 'Select a language by the scope instead of a file extension' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -l config-path -d 'The path to an alternative config.json file' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -s n -l test-number -d 'Query the contents of a specific test' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -s t -l time -d 'Measure execution time' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -s q -l quiet -d 'Suppress main output' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -s c -l captures -d 'Order by captures instead of matches' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -l test -d 'Whether to run query tests or not' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand query" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -l captures-path -d 'The path to a file with captures' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -l query-paths -d 'The paths to files with queries' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -l scope -d 'Select a language by the scope instead of a file extension' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -l paths -d 'The path to a file with paths to source file(s)' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -l config-path -d 'The path to an alternative config.json file' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -s n -l test-number -d 'Highlight the contents of a specific test' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -s H -l html -d 'Generate highlighting as an HTML document' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -l css-classes -d 'When generating HTML, use css classes rather than inline styles' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -l check -d 'Check that highlighting captures conform strictly to standards' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -s t -l time -d 'Measure execution time' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -s q -l quiet -d 'Suppress main output' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand highlight" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand tags" -l scope -d 'Select a language by the scope instead of a file extension' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand tags" -l paths -d 'The path to a file with paths to source file(s)' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand tags" -l config-path -d 'The path to an alternative config.json file' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand tags" -s n -l test-number -d 'Generate tags from the contents of a specific test' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand tags" -s t -l time -d 'Measure execution time' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand tags" -s q -l quiet -d 'Suppress main output' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand tags" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand playground" -l grammar-path -d 'Path to the directory containing the grammar and wasm files' -r +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand playground" -s q -l quiet -d 'Don\'t open in default browser' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand playground" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand dump-languages" -l config-path -d 'The path to an alternative config.json file' -r -F +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand dump-languages" -s h -l help -d 'Print help' +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand complete" -s s -l shell -d 'The shell to generate completions for' -r -f -a "bash\t'' +elvish\t'' +fish\t'' +power-shell\t'' +zsh\t'' +nushell\t''" +complete -c tree-sitter -n "__fish_tree_sitter_using_subcommand complete" -s h -l help -d 'Print help' diff --git a/config/niri/config-desktop.kdl b/config/niri/config-desktop.kdl index 70862d02..0b78ac70 100644 --- a/config/niri/config-desktop.kdl +++ b/config/niri/config-desktop.kdl @@ -2,33 +2,27 @@ // "/-" comments out the following node. // Check the wiki for a full description of the configuration: // https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction - workspace "browser" { open-on-output "DP-1" } - workspace "terminal" { open-on-output "DP-1" } - workspace "chat" { open-on-output "DP-2" } - workspace "music" { open-on-output "DP-2" } - environment { QT_QPA_PLATFORM "wayland" XDG_SESSION_TYPE "wayland" XDG_CURRENT_DESKTOP "niri" XDG_SESSION_DESKTOP "niri" WM "niri" - DISPLAY ":0" // for X11 apps to run - ELECTRON_OZONE_PLATFORM_HINT "auto" + DISPLAY ":0" + // for X11 apps to run ELECTRON_OZONE_PLATFORM_HINT "auto" } - // Input device configuration. // Find the full list of options on the wiki: // https://github.com/YaLTeR/niri/wiki/Configuration:-Input @@ -37,44 +31,36 @@ input { xkb { // You can set rules, model, layout, variant and options. // For more information, see xkeyboard-config(7). - // For example: layout "lv" // options "grp:win_space_toggle,compose:ralt,ctrl:nocaps" options "caps:escape" } - // Enable numlock on startup, omitting this setting disables it. repeat-delay 300 repeat-rate 50 track-layout "global" - // numlock + // numlock } - mouse { // off // natural-scroll accel-speed -0.6 accel-profile "flat" - // scroll-method "no-scroll" + // scroll-method "no-scroll" } - // Uncomment this to make the mouse warp to the center of newly focused windows. warp-mouse-to-focus - // Focus windows and outputs automatically when moving the mouse into them. // Setting max-scroll-amount="0%" makes it work only on windows already fully on screen. focus-follows-mouse max-scroll-amount="95%" } - cursor { // xcursor-theme "breeze_cursors" xcursor-size 16 - hide-when-typing hide-after-inactive-ms 1000 } - // You can configure outputs by their name, which you can find // by running `niri msg outputs` while inside a niri instance. // The built-in laptop monitor is usually called "eDP-1". @@ -84,7 +70,6 @@ cursor { output "DP-1" { // Uncomment this line to disable this output. // off - // Resolution and, optionally, refresh rate of the output. // The format is "x" or "x@". // If the refresh rate is omitted, niri will pick the highest refresh rate @@ -92,14 +77,14 @@ output "DP-1" { // If the mode is omitted altogether or is invalid, niri will pick one automatically. // Run `niri msg outputs` while inside a niri instance to list all outputs and their modes. mode "2560x1440@180.002" - // You can use integer or fractional scale, for example use 1.5 for 150% scale. scale 1 - // Transform allows to rotate the output counter-clockwise, valid values are: // normal, 90, 180, 270, flipped, flipped-90, flipped-180 and flipped-270. transform "normal" - + hot-corners { + off + } // Position of the output in the global coordinate space. // This affects directional monitor actions like "focus-monitor-left", and cursor movement. // The cursor can only move between directly adjacent outputs. @@ -110,34 +95,43 @@ output "DP-1" { // If the position is unset or results in an overlap, the output is instead placed // automatically. position x=0 y=0 - + focus-at-startup variable-refresh-rate on-demand=true background-color "#000" backdrop-color "#000" } - output "DP-2" { mode "1920x1080@74.973" scale 1 - - transform "normal" - - position x=-1920 y=180 - - variable-refresh-rate on-demand=false - focus-at-startup + transform "90" + position x=2560 y=-325 + hot-corners { + off + } + background-color "#000" + backdrop-color "#000" + layout { + default-column-width { + proportion 1.0 + } + } +} +output "HDMI-A-1" { + mode "1920x1080@60" + scale 1 + position x=-1920 y=245 + hot-corners { + off + } background-color "#000" backdrop-color "#000" } - - // Settings that influence how windows are positioned and sized. // Find more information on the wiki: // https://github.com/YaLTeR/niri/wiki/Configuration:-Layout layout { // Set gaps around windows in logical pixels. gaps 4 - // When to center a column when changing focus, options are: // - "never", default behavior, focusing an off-screen column will keep at the left // or right edge of the screen. @@ -145,7 +139,6 @@ layout { // - "on-overflow", focusing a column will center it if it doesn't fit // together with the previously focused column. center-focused-column "never" - // You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between. preset-column-widths { // Proportion sets the width as a fraction of the output width, taking gaps into account. @@ -154,19 +147,17 @@ layout { proportion 0.33333 proportion 0.5 proportion 0.66667 - - // Fixed sets the width in logical pixels exactly. + // Fixed sets the width in logical pixels exactly. // fixed 1920 } - // You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between. // preset-window-heights { } - // You can change the default width of the new windows. - default-column-width { proportion 0.5; } + default-column-width { + proportion 0.5 + } // If you leave the brackets empty, the windows themselves will decide their initial width. // default-column-width {} - // By default focus ring and border are rendered as a solid background rectangle // behind windows. That is, they will show up through semitransparent windows. // This is because windows using client-side decorations can have an arbitrary shape. @@ -177,27 +168,21 @@ layout { // // Alternatively, you can override it with a window rule called // `draw-border-with-background`. - // You can change how the focus ring looks. focus-ring { // Uncomment this line to disable the focus ring. off - // How many logical pixels the ring extends out from the windows. width 2 - // Colors can be set in a variety of ways: // - CSS named colors: "red" // - RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa" // - CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others. - // Color of the ring on the active monitor. active-color "#ebbcba" - // Color of the ring on inactive monitors. inactive-color "#6e6a86" - - // You can also use gradients. They take precedence over solid colors. + // You can also use gradients. They take precedence over solid colors. // Gradients are rendered the same as CSS linear-gradient(angle, from, to). // The angle is the same as in linear-gradient, and is optional, // defaulting to 180 (top-to-bottom gradient). @@ -212,29 +197,23 @@ layout { // // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" } - // You can also add a border. It's similar to the focus ring, but always visible. border { // The settings are the same as for the focus ring. // If you enable the border, you probably want to disable the focus ring. // off - width 2 active-color "#ebbcba" inactive-color "#6e6a86" - // Color of the border around windows that request your attention. urgent-color "#eb6f92" - - // active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view" + // active-gradient from="#ffbb66" to="#ffc880" angle=45 relative-to="workspace-view" // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" } - // You can enable drop shadows for windows. shadow { // Uncomment the next line to enable shadows. // on - // By default, the shadow draws only around its window, and not behind it. // Uncomment this setting to make the shadow draw behind its window. // @@ -250,41 +229,34 @@ layout { // draws any. // // draw-behind-window true - // You can change how shadows look. The values below are in logical // pixels and match the CSS box-shadow properties. - // Softness controls the shadow blur radius. softness 30 - // Spread expands the shadow. spread 5 - // Offset moves the shadow relative to the window. offset x=0 y=5 - // You can also change the shadow color and opacity. color "#0007" } - // Struts shrink the area occupied by windows, similarly to layer-shell panels. // You can think of them as a kind of outer gaps. They are set in logical pixels. // Left and right struts will cause the next window to the side to always be visible. // Top and bottom struts will simply add outer gaps in addition to the area occupied by // layer-shell panels and regular gaps. struts { - // left 64 + // left 64 // right 64 // top 64 // bottom 64 + } } - // Add lines like this to spawn processes at startup. // Note that running niri as a session supports xdg-desktop-autostart, // which may be more convenient to use. // See the binds section below for more spawn examples. - // This line starts waybar, a commonly used bar for Wayland compositors. spawn-at-startup "pipewire" spawn-at-startup "pipewire-pulse" @@ -305,38 +277,31 @@ spawn-at-startup "AyuGram" spawn-at-startup "discord" spawn-at-startup "swap-wallpaper" spawn-at-startup "spotify-launcher" - // Uncomment this line to ask the clients to omit their client-side decorations if possible. // If the client will specifically ask for CSD, the request will be honored. // Additionally, clients will be informed that they are tiled, removing some client-side rounded corners. // This option will also fix border/focus ring drawing behind some semitransparent windows. // After enabling or disabling this, you need to restart the apps for this to take effect. prefer-no-csd - // You can change the path where screenshots are saved. // A ~ at the front will be expanded to the home directory. // The path is formatted with strftime(3) to give you the screenshot date and time. screenshot-path "~/Pictures/screenshots/%Y-%m-%d_%H-%M-%S.png" - // You can also set this to null to disable saving screenshots to disk. // screenshot-path null - // Animation settings. // The wiki explains how to configure individual animations: // https://github.com/YaLTeR/niri/wiki/Configuration:-Animations animations { // Uncomment to turn off all animations. off - // Slow down all animations by this factor. Values below 1 speed them up instead. slowdown 3.0 } - layer-rule { match namespace="^notifications$" block-out-from "screencast" } - window-rule { draw-border-with-background false } @@ -350,90 +315,80 @@ window-rule { // This regular expression is intentionally made as specific as possible, // since this is the default config, and we want no false positives. // You can get away with just app-id="wezterm" if you want. - match app-id=r#"^org\.wezfurlong\.wezterm$"# - default-column-width {} -} + match app-id="^org\\.wezfurlong\\.wezterm$" + default-column-width { + } +} // Open the Firefox picture-in-picture player as floating by default. window-rule { // This app-id regular expression will work for both: // - host Firefox (app-id is "firefox") // - Flatpak Firefox (app-id is "org.mozilla.firefox") - match app-id=r#"firefox$"# title="^Picture-in-Picture$" - match app-id=r#"floorp-default"# title="^Picture-in-Picture$" + match app-id="firefox$" title="^Picture-in-Picture$" + match app-id="floorp-default" title="^Picture-in-Picture$" open-floating true } - window-rule { - match title=r#"^Extension:.*Bitwarden.*"# - + match title="^Extension:.*Bitwarden.*" default-floating-position x=10 y=10 relative-to="top-right" } - window-rule { - match app-id="steam" title=r#"^notificationtoasts_\d+_desktop$"# + match app-id="steam" title="^notificationtoasts_\\d+_desktop$" default-floating-position x=10 y=10 relative-to="bottom-right" } - // Example: block out two password managers from screen capture. // (This example rule is commented out with a "/-" in front.) window-rule { - match app-id=r#"^org\.keepassxc\.KeePassXC$"# - match app-id=r#"^org\.gnome\.World\.Secrets$"# - match app-id=r#"^org\.gnome\.World\.Secrets$"# + match app-id="^org\\.keepassxc\\.KeePassXC$" + match app-id="^org\\.gnome\\.World\\.Secrets$" + match app-id="^org\\.gnome\\.World\\.Secrets$" match title="Bitwarden" - block-out-from "screen-capture" - - // Use this instead if you want them visible on third-party screenshot tools. + // Use this instead if you want them visible on third-party screenshot tools. // block-out-from "screencast" } - window-rule { match at-startup=true app-id="floorp-default" match at-startup=true app-id="floorp" open-on-workspace "browser" open-maximized true } - window-rule { - match at-startup=true app-id=r#"^org\.telegram\.desktop$"# - match at-startup=true app-id=r#"^com\.ayugram\.desktop$"# - match at-startup=true app-id=r#"^org\.gnome\.Fractal$"# - match at-startup=true app-id=r#"discord"# - match at-startup=true app-id=r#"vesktop"# - - exclude app-id=r#"^com\.ayugram\.desktop$"# title="^Media viewer$" - exclude app-id=r#"^org\.telegram\.desktop$"# title="^Media viewer$" + match at-startup=true app-id="^org\\.telegram\\.desktop$" + match at-startup=true app-id="^com\\.ayugram\\.desktop$" + match at-startup=true app-id="^org\\.gnome\\.Fractal$" + match at-startup=true app-id="discord" + match at-startup=true app-id="vesktop" + match at-startup=true app-id="Element" + match at-startup=true app-id="^org\\.telegram\\.desktop$" + match at-startup=true app-id="^com\\.ayugram\\.desktop$" + exclude app-id="^com\\.ayugram\\.desktop$" title="^Media viewer$" + exclude app-id="^org\\.telegram\\.desktop$" title="^Media viewer$" open-on-workspace "chat" } window-rule { - match app-id=r#"^org\.telegram\.desktop$"# - match app-id=r#"^com\.ayugram\.desktop$"# - match app-id=r#"^org\.gnome\.Fractal$"# - match app-id=r#"discord"# - match app-id=r#"vesktop"# - match app-id=r#"spotify"# - + match app-id="^org\\.telegram\\.desktop$" + match app-id="^com\\.ayugram\\.desktop$" + match app-id="^org\\.gnome\\.Fractal$" + match app-id="discord" + match app-id="vesktop" + match app-id="spotify" + match app-id="Element" opacity 0.95 } - window-rule { match app-id="mpv" open-fullscreen true } - window-rule { - match app-id=r#"^com\.ayugram\.desktop$"# title="^Media viewer$" - match app-id=r#"^org\.telegram\.desktop$"# title="^Media viewer$" - + match app-id="^com\\.ayugram\\.desktop$" title="^Media viewer$" + match app-id="^org\\.telegram\\.desktop$" title="^Media viewer$" open-maximized true } - window-rule { match at-startup=true app-id="spotify" open-maximized true - open-on-workspace "music" } window-rule { @@ -443,14 +398,12 @@ window-rule { open-floating true open-focused true } - // Example: enable rounded corners for all windows. // (This example rule is commented out with a "/-" in front.) /-window-rule { - geometry-corner-radius 12 - clip-to-geometry true +geometry-corner-radius 12 +clip-to-geometry true } - binds { // Keys consist of modifiers separated by + signs, followed by an XKB key name // in the end. To find an XKB name for a particular key, you may use a program @@ -461,115 +414,195 @@ binds { // // Most actions that you can bind here can also be invoked programmatically with // `niri msg action do-something`. - // Mod-Shift-/, which is usually the same as Mod-?, // shows a list of important hotkeys. - Mod+Shift+Slash { show-hotkey-overlay; } - + Mod+Shift+Slash { + show-hotkey-overlay + } // Suggested binds for running programs: terminal, app launcher, screen locker. - Mod+Return hotkey-overlay-title="Open a Terminal: {{terminal}}" { spawn "{{terminal}}"; } - Mod+P hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; } + Mod+Return hotkey-overlay-title="Open a Terminal: {{terminal}}" { + spawn "{{terminal}}" + } + Mod+P hotkey-overlay-title="Run an Application: fuzzel" { + spawn "fuzzel" + } // Super+Space allow-when-locked=true hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; } // Super+Space hotkey-overlay-title="Lock the Screen: hyprlock" { spawn "hyprlock"; } - // You can also use a shell. Do this if you need pipes, multiple commands, etc. // Note: the entire command goes as a single argument in the end. // Mod+T { spawn "bash" "-c" "notify-send hello && exec alacritty"; } - // Example volume keys mappings for PipeWire & WirePlumber. // The allow-when-locked=true property makes them work even when the session is locked. - XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume $(get-spotify-id) 0.02+"; } - XF86AudioLowerVolume allow-when-locked=true { spawn-sh "wpctl set-volume $(get-spotify-id) 0.02-"; } - XF86AudioMute allow-when-locked=true { spawn "sp" "play"; } - XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; } - Page_Down allow-when-locked=true {spawn "sp" "next"; } - Page_Up allow-when-locked=true {spawn "sp" "prev"; } - + XF86AudioRaiseVolume allow-when-locked=true { + spawn-sh "wpctl set-volume $(get-spotify-id) 0.02+" + } + XF86AudioLowerVolume allow-when-locked=true { + spawn-sh "wpctl set-volume $(get-spotify-id) 0.02-" + } + XF86AudioMute allow-when-locked=true { + spawn "sp" "play" + } + XF86AudioMicMute allow-when-locked=true { + spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle" + } + Page_Down allow-when-locked=true { + spawn "sp" "next" + } + Page_Up allow-when-locked=true { + spawn "sp" "prev" + } // Open/close the Overview: a zoomed-out view of workspaces and windows. // You can also move the mouse into the top-left hot corner, // or do a four-finger swipe up on a touchpad. - Mod+O repeat=false { toggle-overview; } - - Mod+Shift+Q { close-window; } - - Mod+H { focus-column-left; } - Mod+J { focus-window-down; } - Mod+K { focus-window-up; } - Mod+L { focus-column-right; } - - Mod+Shift+H { move-column-left; } - Mod+Shift+J { move-window-down; } - Mod+Shift+K { move-window-up; } - Mod+Shift+L { move-column-right; } - + Mod+O repeat=false { + toggle-overview + } + Mod+Shift+Q { + close-window + } + Mod+H { + focus-column-left + } + Mod+J { + focus-window-down + } + Mod+K { + focus-window-up + } + Mod+L { + focus-column-right + } + Mod+Shift+H { + move-column-left + } + Mod+Shift+J { + move-window-down + } + Mod+Shift+K { + move-window-up + } + Mod+Shift+L { + move-column-right + } // Alternative commands that move across workspaces when reaching // the first or last window in a column. // Mod+J { focus-window-or-workspace-down; } // Mod+K { focus-window-or-workspace-up; } // Mod+Ctrl+J { move-window-down-or-to-workspace-down; } // Mod+Ctrl+K { move-window-up-or-to-workspace-up; } - - Mod+Home { focus-column-first; } - Mod+End { focus-column-last; } - Mod+Ctrl+Home { move-column-to-first; } - Mod+Ctrl+End { move-column-to-last; } - - Mod+Ctrl+H { focus-monitor-left; } - Mod+Ctrl+J { focus-monitor-down; } - Mod+Ctrl+K { focus-monitor-up; } - Mod+Ctrl+L { focus-monitor-right; } - - Mod+Shift+Ctrl+H { move-column-to-monitor-left; } - Mod+Shift+Ctrl+J { move-column-to-monitor-down; } - Mod+Shift+Ctrl+K { move-column-to-monitor-up; } - Mod+Shift+Ctrl+L { move-column-to-monitor-right; } - + Mod+Home { + focus-column-first + } + Mod+End { + focus-column-last + } + Mod+Ctrl+Home { + move-column-to-first + } + Mod+Ctrl+End { + move-column-to-last + } + Mod+Ctrl+H { + focus-monitor-left + } + Mod+Ctrl+J { + focus-monitor-down + } + Mod+Ctrl+K { + focus-monitor-up + } + Mod+Ctrl+L { + focus-monitor-right + } + Mod+Shift+Ctrl+H { + move-column-to-monitor-left + } + Mod+Shift+Ctrl+J { + move-column-to-monitor-down + } + Mod+Shift+Ctrl+K { + move-column-to-monitor-up + } + Mod+Shift+Ctrl+L { + move-column-to-monitor-right + } // Alternatively, there are commands to move just a single window: // Mod+Shift+Ctrl+Left { move-window-to-monitor-left; } // ... - // And you can also move a whole workspace to another monitor: // Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; } // ... - - Mod+Down { focus-workspace-down; } - Mod+Up { focus-workspace-up; } - Mod+Ctrl+Down { move-column-to-workspace-down; } - Mod+Ctrl+Up { move-column-to-workspace-up; } - - Mod+Alt+J { focus-workspace-down; } - Mod+Alt+K { focus-workspace-up; } - + Mod+Down { + focus-workspace-down + } + Mod+Up { + focus-workspace-up + } + Mod+Ctrl+Down { + move-column-to-workspace-down + } + Mod+Ctrl+Up { + move-column-to-workspace-up + } + Mod+Alt+J { + focus-workspace-down + } + Mod+Alt+K { + focus-workspace-up + } // Alternatively, there are commands to move just a single window: // Mod+Ctrl+Page_Down { move-window-to-workspace-down; } // ... - - Mod+Shift+Down { move-workspace-down; } - Mod+Shift+Up { move-workspace-up; } - + Mod+Shift+Down { + move-workspace-down + } + Mod+Shift+Up { + move-workspace-up + } // You can bind mouse wheel scroll ticks using the following syntax. // These binds will change direction based on the natural-scroll setting. // // To avoid scrolling through workspaces really fast, you can use // the cooldown-ms property. The bind will be rate-limited to this value. // You can set a cooldown on any bind, but it's most useful for the wheel. - Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; } - Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; } - Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; } - Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; } - - Mod+WheelScrollRight { focus-column-right; } - Mod+WheelScrollLeft { focus-column-left; } - Mod+Ctrl+WheelScrollRight { move-column-right; } - Mod+Ctrl+WheelScrollLeft { move-column-left; } - + Mod+WheelScrollDown cooldown-ms=150 { + focus-workspace-down + } + Mod+WheelScrollUp cooldown-ms=150 { + focus-workspace-up + } + Mod+Ctrl+WheelScrollDown cooldown-ms=150 { + move-column-to-workspace-down + } + Mod+Ctrl+WheelScrollUp cooldown-ms=150 { + move-column-to-workspace-up + } + Mod+WheelScrollRight { + focus-column-right + } + Mod+WheelScrollLeft { + focus-column-left + } + Mod+Ctrl+WheelScrollRight { + move-column-right + } + Mod+Ctrl+WheelScrollLeft { + move-column-left + } // Usually scrolling up and down with Shift in applications results in // horizontal scrolling; these binds replicate that. - Mod+Shift+WheelScrollDown { focus-column-right; } - Mod+Shift+WheelScrollUp { focus-column-left; } - Mod+Ctrl+Shift+WheelScrollDown { move-column-right; } - Mod+Ctrl+Shift+WheelScrollUp { move-column-left; } - + Mod+Shift+WheelScrollDown { + focus-column-right + } + Mod+Shift+WheelScrollUp { + focus-column-left + } + Mod+Ctrl+Shift+WheelScrollDown { + move-column-right + } + Mod+Ctrl+Shift+WheelScrollUp { + move-column-left + } // Similarly, you can bind touchpad scroll "ticks". // Touchpad scrolling is continuous, so for these binds it is split into // discrete intervals. @@ -578,7 +611,6 @@ binds { // touchpads by default. // Mod+TouchpadScrollDown { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02+"; } // Mod+TouchpadScrollUp { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "0.02-"; } - // You can refer to workspaces by index. However, keep in mind that // niri is a dynamic workspace system, so these commands are kind of // "best effort". Trying to refer to a workspace index bigger than @@ -587,57 +619,104 @@ binds { // // For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on // will all refer to the 3rd workspace. - Mod+1 { focus-workspace "browser"; } - Mod+2 { focus-workspace "terminal"; } - Mod+3 { focus-workspace 3; } - Mod+4 { focus-workspace 4; } - Mod+5 { focus-workspace 5; } - Mod+6 { focus-workspace 6; } - Mod+7 { focus-workspace 7; } - Mod+8 { focus-workspace "chat"; } - Mod+9 { focus-workspace "music"; } - Mod+Shift+1 { move-column-to-workspace "browser"; } - Mod+Shift+2 { move-column-to-workspace "terminal"; } - Mod+Shift+3 { move-column-to-workspace 3; } - Mod+Shift+4 { move-column-to-workspace 4; } - Mod+Shift+5 { move-column-to-workspace 5; } - Mod+Shift+6 { move-column-to-workspace 6; } - Mod+Shift+7 { move-column-to-workspace 7; } - Mod+Shift+8 { move-column-to-workspace "chat"; } - Mod+Shift+9 { move-column-to-workspace "music"; } - + Mod+1 { + focus-workspace "browser" + } + Mod+2 { + focus-workspace "terminal" + } + Mod+3 { + focus-workspace 3 + } + Mod+4 { + focus-workspace 4 + } + Mod+5 { + focus-workspace 5 + } + Mod+6 { + focus-workspace 6 + } + Mod+7 { + focus-workspace 7 + } + Mod+8 { + focus-workspace "chat" + } + Mod+9 { + focus-workspace "music" + } + Mod+Shift+1 { + move-column-to-workspace "browser" + } + Mod+Shift+2 { + move-column-to-workspace "terminal" + } + Mod+Shift+3 { + move-column-to-workspace 3 + } + Mod+Shift+4 { + move-column-to-workspace 4 + } + Mod+Shift+5 { + move-column-to-workspace 5 + } + Mod+Shift+6 { + move-column-to-workspace 6 + } + Mod+Shift+7 { + move-column-to-workspace 7 + } + Mod+Shift+8 { + move-column-to-workspace "chat" + } + Mod+Shift+9 { + move-column-to-workspace "music" + } // Alternatively, there are commands to move just a single window: // Mod+Ctrl+1 { move-window-to-workspace 1; } - // Switches focus between the current and the previous workspace. // Mod+Tab { focus-workspace-previous; } - // The following binds move the focused window in and out of a column. // If the window is alone, they will consume it into the nearby column to the side. // If the window is already in a column, they will expel it out. - Mod+Comma { consume-or-expel-window-left; } - Mod+Period { consume-or-expel-window-right; } - + Mod+Comma { + consume-or-expel-window-left + } + Mod+Period { + consume-or-expel-window-right + } // Consume one window from the right to the bottom of the focused column. // Mod+Comma { consume-window-into-column; } // Expel the bottom window from the focused column to the right. // Mod+Period { expel-window-from-column; } - - Mod+R { switch-preset-column-width; } - Mod+Shift+R { switch-preset-window-height; } - Mod+Ctrl+R { reset-window-height; } - Mod+M { maximize-column; } - Mod+F { fullscreen-window; } - + Mod+R { + switch-preset-column-width + } + Mod+Shift+R { + switch-preset-window-height + } + Mod+Ctrl+R { + reset-window-height + } + Mod+M { + maximize-column + } + Mod+F { + fullscreen-window + } // Expand the focused column to space not taken up by other fully visible columns. // Makes the column "fill the rest of the space". - Mod+Ctrl+F { expand-column-to-available-width; } - - Mod+C { center-column; } - + Mod+Ctrl+F { + expand-column-to-available-width + } + Mod+C { + center-column + } // Center all fully visible columns on screen. - Mod+Ctrl+C { center-visible-columns; } - + Mod+Ctrl+C { + center-visible-columns + } // Finer width adjustments. // This command can also: // * set width in pixels: "1000" @@ -646,24 +725,33 @@ binds { // * adjust width as a percentage of screen width: "-10%" or "+10%" // Pixel sizes use logical, or scaled, pixels. I.e. on an output with scale 2.0, // set-column-width "100" will make the column occupy 200 physical screen pixels. - Mod+Minus { set-column-width "-10%"; } - Mod+Equal { set-column-width "+10%"; } - + Mod+Minus { + set-column-width "-10%" + } + Mod+Equal { + set-column-width "+10%" + } // Finer height adjustments when in column with other windows. - Mod+Shift+Minus { set-window-height "-10%"; } - Mod+Shift+Equal { set-window-height "+10%"; } - + Mod+Shift+Minus { + set-window-height "-10%" + } + Mod+Shift+Equal { + set-window-height "+10%" + } // Move the focused window between the floating and the tiling layout. - Mod+Ctrl+Space { toggle-window-floating; } - Mod+Shift+V { switch-focus-between-floating-and-tiling; } - + Mod+Ctrl+Space { + toggle-window-floating + } + Mod+Shift+V { + switch-focus-between-floating-and-tiling + } // Mod+Shift+Space { spawn "nsticky" "sticky" "toggle-active"; } - // Toggle tabbed column display mode. // Windows in this column will appear as vertical tabs, // rather than stacked on top of each other. - Mod+W { toggle-column-tabbed-display; } - + Mod+W { + toggle-column-tabbed-display + } // Actions to switch layouts. // Note: if you uncomment these, make sure you do NOT have // a matching layout switch hotkey configured in xkb options above. @@ -671,11 +759,15 @@ binds { // since it will switch twice upon pressing the hotkey (once by xkb, once by niri). // Mod+Space { switch-layout "next"; } // Mod+Shift+Space { switch-layout "prev"; } - - Mod+Delete { screenshot; } - Mod+Shift+Delete { screenshot-screen; } - Mod+Alt+Delete { screenshot-window; } - + Mod+Delete { + screenshot + } + Mod+Shift+Delete { + screenshot-screen + } + Mod+Alt+Delete { + screenshot-window + } // Applications such as remote-desktop clients and software KVM switches may // request that niri stops processing the keyboard shortcuts defined here // so they may, for example, forward the key presses as-is to a remote machine.niri @@ -684,21 +776,27 @@ binds { // // The allow-inhibiting=false property can be applied to other binds as well, // which ensures niri always processes them, even when an inhibitor is active. - Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; } - + Mod+Escape allow-inhibiting=false { + toggle-keyboard-shortcuts-inhibit + } // The quit action will show a confirmation dialog to avoid accidental exits. - Mod+Shift+E { quit; } - Ctrl+Alt+Delete { quit; } - + Mod+Shift+E { + quit + } + Ctrl+Alt+Delete { + quit + } // Powers off the monitors. To turn them back on, do any input like // moving the mouse or pressing any other key. // Mod+Shift+P { power-off-monitors; } - - Mod+Shift+P { spawn-sh "swap-wallpaper -n"; } - Mod+B { spawn "{{browser}}"; } + Mod+Shift+P { + spawn-sh "swap-wallpaper -n" + } + Mod+B { + spawn "{{browser}}" + } } - - hotkey-overlay { skip-at-startup } + diff --git a/config/tmThemes/Rosé Pine Dawn.tmTheme b/config/tmThemes/rose-pine-dawn.tmTheme similarity index 87% rename from config/tmThemes/Rosé Pine Dawn.tmTheme rename to config/tmThemes/rose-pine-dawn.tmTheme index 6c94c8c1..f9dc874f 100644 --- a/config/tmThemes/Rosé Pine Dawn.tmTheme +++ b/config/tmThemes/rose-pine-dawn.tmTheme @@ -30,6 +30,8 @@ comment settings + fontStyle + italic foreground #797593 @@ -38,7 +40,7 @@ name String scope - string + string, punctuation.definition.string settings foreground @@ -53,7 +55,7 @@ settings foreground - #907aa9 + #ea9d34 @@ -63,8 +65,10 @@ constant.language settings + fontStyle + bold foreground - #907aa9 + #ea9d34 @@ -75,7 +79,7 @@ settings foreground - #907aa9 + #ea9d34 @@ -86,9 +90,9 @@ settings fontStyle - + italic foreground - #d7827e + #575279 @@ -99,7 +103,7 @@ settings foreground - #b4637a + #286983 @@ -112,7 +116,7 @@ fontStyle foreground - #b4637a + #56949f @@ -123,7 +127,7 @@ settings fontStyle - italic + foreground #56949f @@ -136,7 +140,7 @@ settings fontStyle - bold + bold foreground #286983 @@ -162,9 +166,9 @@ settings fontStyle - + italic foreground - #286983 + #d7827e @@ -175,9 +179,9 @@ settings fontStyle - italic + foreground - #ea9d34 + #907aa9 @@ -188,9 +192,9 @@ settings fontStyle - + bold foreground - #b4637a + #286983 @@ -203,7 +207,7 @@ fontStyle foreground - #286983 + #907aa9 @@ -214,9 +218,9 @@ settings fontStyle - + bold foreground - #56949f + #d7827e @@ -227,9 +231,9 @@ settings fontStyle - + bold foreground - #56949f + #ea9d34 @@ -240,7 +244,7 @@ settings fontStyle - italic + bold foreground #56949f @@ -253,7 +257,9 @@ settings fontStyle - + bold + foreground + #b4637a @@ -284,16 +290,27 @@ #575279 + + name + Punctuation, Operators + scope + punctuation, keyword.operator + settings + + foreground + #797593 + + - uuid - dac29768-bcff-4df3-936c-88c7540d550d colorSpaceName sRGB semanticClass - theme.light.rosé_pine-dawn + theme.light.rose-pine-dawn author - oplik0 + arrrgi comment - soho vibes - modified from the sublime text theme by ThatOneCalculator + All natural pine, faux fur and a bit of soho vibes for the classy minimalist + uuid + BB4B4616-E742-41D5-BB5B-63D45FA614F diff --git a/config/tmThemes/Rosé Pine Moon.tmTheme b/config/tmThemes/rose-pine-moon.tmTheme similarity index 86% rename from config/tmThemes/Rosé Pine Moon.tmTheme rename to config/tmThemes/rose-pine-moon.tmTheme index d96a7e1a..7872c8ee 100644 --- a/config/tmThemes/Rosé Pine Moon.tmTheme +++ b/config/tmThemes/rose-pine-moon.tmTheme @@ -20,7 +20,7 @@ lineHighlight #2a283e selection - #6e6a86 + #44415a @@ -30,6 +30,8 @@ comment settings + fontStyle + italic foreground #908caa @@ -38,7 +40,7 @@ name String scope - string + string, punctuation.definition.string settings foreground @@ -53,7 +55,7 @@ settings foreground - #c4a7e7 + #f6c177 @@ -63,8 +65,10 @@ constant.language settings + fontStyle + bold foreground - #c4a7e7 + #f6c177 @@ -75,7 +79,7 @@ settings foreground - #c4a7e7 + #f6c177 @@ -86,9 +90,9 @@ settings fontStyle - + italic foreground - #ea9a97 + #e0def4 @@ -99,7 +103,7 @@ settings foreground - #eb6f92 + #3e8fb0 @@ -112,7 +116,7 @@ fontStyle foreground - #eb6f92 + #9ccfd8 @@ -123,7 +127,7 @@ settings fontStyle - italic + foreground #9ccfd8 @@ -136,7 +140,7 @@ settings fontStyle - bold + bold foreground #3e8fb0 @@ -162,9 +166,9 @@ settings fontStyle - + italic foreground - #3e8fb0 + #ea9a97 @@ -175,9 +179,9 @@ settings fontStyle - italic + foreground - #f6c177 + #c4a7e7 @@ -188,9 +192,9 @@ settings fontStyle - + bold foreground - #eb6f92 + #3e8fb0 @@ -203,7 +207,7 @@ fontStyle foreground - #3e8fb0 + #c4a7e7 @@ -214,9 +218,9 @@ settings fontStyle - + bold foreground - #9ccfd8 + #ea9a97 @@ -227,9 +231,9 @@ settings fontStyle - + bold foreground - #9ccfd8 + #f6c177 @@ -240,7 +244,7 @@ settings fontStyle - italic + bold foreground #9ccfd8 @@ -253,7 +257,9 @@ settings fontStyle - + bold + foreground + #eb6f92 @@ -284,16 +290,27 @@ #e0def4 + + name + Punctuation, Operators + scope + punctuation, keyword.operator + settings + + foreground + #908caa + + - uuid - a65f621e-b84b-48fb-afec-e5c085e8debf colorSpaceName sRGB semanticClass - theme.dark.rosé_pine-moon + theme.dark.rose-pine-moon author - oplik0 + arrrgi comment - soho vibes - modified from the sublime text theme by ThatOneCalculator + All natural pine, faux fur and a bit of soho vibes for the classy minimalist + uuid + CC28B8FB-96BA-43EB-B71F-5AA3D3EBB0BB diff --git a/config/tmThemes/Rosé Pine.tmTheme b/config/tmThemes/rose-pine.tmTheme similarity index 87% rename from config/tmThemes/Rosé Pine.tmTheme rename to config/tmThemes/rose-pine.tmTheme index 7dea0f15..79cceb54 100644 --- a/config/tmThemes/Rosé Pine.tmTheme +++ b/config/tmThemes/rose-pine.tmTheme @@ -30,6 +30,8 @@ comment settings + fontStyle + italic foreground #908caa @@ -38,7 +40,7 @@ name String scope - string + string, punctuation.definition.string settings foreground @@ -53,7 +55,7 @@ settings foreground - #c4a7e7 + #f6c177 @@ -63,8 +65,10 @@ constant.language settings + fontStyle + bold foreground - #c4a7e7 + #f6c177 @@ -75,7 +79,7 @@ settings foreground - #c4a7e7 + #f6c177 @@ -86,9 +90,9 @@ settings fontStyle - + italic foreground - #ebbcba + #e0def4 @@ -99,7 +103,7 @@ settings foreground - #eb6f92 + #31748f @@ -112,7 +116,7 @@ fontStyle foreground - #eb6f92 + #9ccfd8 @@ -123,7 +127,7 @@ settings fontStyle - italic + foreground #9ccfd8 @@ -136,7 +140,7 @@ settings fontStyle - bold + bold foreground #31748f @@ -162,9 +166,9 @@ settings fontStyle - + italic foreground - #31748f + #ebbcba @@ -175,9 +179,9 @@ settings fontStyle - italic + foreground - #f6c177 + #c4a7e7 @@ -188,9 +192,9 @@ settings fontStyle - + bold foreground - #eb6f92 + #31748f @@ -203,7 +207,7 @@ fontStyle foreground - #31748f + #c4a7e7 @@ -214,9 +218,9 @@ settings fontStyle - + bold foreground - #9ccfd8 + #ebbcba @@ -227,9 +231,9 @@ settings fontStyle - + bold foreground - #9ccfd8 + #f6c177 @@ -240,7 +244,7 @@ settings fontStyle - italic + bold foreground #9ccfd8 @@ -253,7 +257,9 @@ settings fontStyle - + bold + foreground + #eb6f92 @@ -284,16 +290,27 @@ #e0def4 + + name + Punctuation, Operators + scope + punctuation, keyword.operator + settings + + foreground + #908caa + + - uuid - c3af112c-80e3-45fe-8890-43ca225fda21 colorSpaceName sRGB semanticClass - theme.dark.rosé_pine + theme.dark.rose-pine author - oplik0 + arrrgi comment - soho vibes - modified from the sublime text theme by ThatOneCalculator + All natural pine, faux fur and a bit of soho vibes for the classy minimalist + uuid + 14991673-80EB-41A2-BEFF-03216A233730 diff --git a/config/waybar/config.bak b/config/waybar/config.bak deleted file mode 100644 index bbb944e6..00000000 --- a/config/waybar/config.bak +++ /dev/null @@ -1,236 +0,0 @@ -{ - "layer": "top", // Waybar at top layer - // "position": "bottom", // Waybar position (top|bottom|left|right) - "height": 30, // Waybar height (to be removed for auto height) - // "width": 1280, // Waybar width - "spacing": 4, // Gaps between modules (4px) - // Choose the order of the modules - "modules-left": ["wlr/workspaces", "custom/media"], - "modules-center": ["hyprland/window"], - "modules-right": ["network", "custom/pacman", "backlight", "temperature", "pulseaudio", "battery", "custom/dunst", "custom/weather", "tray", "clock"], - // Modules configuration - "wlr/workspaces": { - "format": "{icon}", - "on-click": "activate", - "format-icons": { - // "1": "1", - // "2": "2", - // "3": "3", - // "4": "4", - // "5": "5", - // "6": "6", - // "7": "7", - // "8": "8", - // "9": "9", - // "10": "10", - // "urgent": "", - // "active": "", - // "default": "" - }, - "sort-by-number": true - }, - - "hyprland/window": { - "format": "{}", - "separate-outputs": true - }, - - "hyprland/language": { - "format": "{}", - "format-us": "us", - "format-lv": "lv", - "keyboard-name": "AT Translated Set 2 keyboard" - }, - - "keyboard-state": { - "numlock": true, - "capslock": true, - "format": "{name} {icon}", - "format-icons": { - "locked": "", - "unlocked": "" - } - }, - - "mpd": { - "format": "{stateIcon} {consumeIcon}{randomIcon}{repeatIcon}{singleIcon}{artist} - {album} - {title} ({elapsedTime:%M:%S}/{totalTime:%M:%S}) ⸨{songPosition}|{queueLength}⸩ {volume}% ", - "format-disconnected": "Disconnected ", - "format-stopped": "{consumeIcon}{randomIcon}{repeatIcon}{singleIcon}Stopped ", - "unknown-tag": "N/A", - "interval": 2, - "consume-icons": { - "on": " " - }, - "random-icons": { - "off": " ", - "on": " " - }, - "repeat-icons": { - "on": " " - }, - "single-icons": { - "on": "1 " - }, - "state-icons": { - "paused": "", - "playing": "" - }, - "tooltip-format": "MPD (connected)", - "tooltip-format-disconnected": "MPD (disconnected)" - }, - - "idle_inhibitor": { - "format": "{icon}", - "format-icons": { - "activated": "", - "deactivated": "" - } - }, - - "tray": { - "icon-size": 21, - "spacing": 10 - }, - - "clock": { - "format": "{:%d.%m.%Y %H:%M:%S}", - "tooltip-format": "{:%Y %B}\n{calendar}", - "interval": 1 - }, - - "cpu": { - "format": "{usage}% ", - "tooltip": false - }, - - "memory": { - "interval": 30, - "format": "{used:0.1f}G/{total:0.1f}G" - }, - - "temperature": { - // "thermal-zone": 2, - // "hwmon-path": "/sys/class/hwmon/hwmon2/temp1_input", - "critical-threshold": 80, - "format-critical": "{temperatureC}°C ", - "format": "{temperatureC}°C ", - "format-icons": ["", "", ""] - }, - - "backlight": { - // "device": "acpi_video1", - "format": "{percent}% {icon}", - "format-icons": ["", "", "", "", "", "", "", "", ""], - "on-click": "brightnessctl -q set 60%", - "on-scroll-down": "brightnessctl -q set +1%", - "on-scroll-up": "brightnessctl -q set 1%-", - - }, - - "battery": { - "states": { - "good": 95, - "warning": 30, - "critical": 10 - }, - "format": "{capacity}% {icon}", - "format-charging": "{capacity}% ", - "format-plugged": "{capacity}% ", - "format-alt": "{time} {icon}", - // "format-good": "", // An empty format will hide the module - // "format-full": "", - "format-icons": ["", "", "", "", ""] - }, - - "network": { - "format-wifi": "{essid} ({signalStrength}%) ", - "format-ethernet": "{ipaddr}/{cidr} ", - "tooltip-format": "{ifname} via {gwaddr} ", - "format-linked": "{ifname} (No IP) ", - "format-disconnected": "Disconnected ⚠", - "format-alt": "{ifname}: {ipaddr}/{cidr}" - }, - - "wireplumber": { - "format": "{volume}% {icon}", - "format-muted": "", - "on-click": "pulsemixer --toggle-mute", - "format-icons": ["", "", ""] - }, - - "pulseaudio": { - "scroll-step": 1, // %, can be a float - "format": "{volume}% {icon} {format_source}", - "format-bluetooth": "{volume}% {icon} {format_source}", - "format-bluetooth-muted": "{volume}% {icon} {format_source}", - "format-muted": "{volume}% {icon} {format_source}", - "format-source": "{volume}% ", - "format-source-muted": "", - "format-icons": { - "headphone": "", - "hands-free": "", - "headset": "", - "phone": "", - "portable": "", - "car": "", - "default": ["", "", ""] - }, - "on-click": "pulsemixer --toggle-mute" - }, - - "custom/media": { - "format": "{icon} {}", - "return-type": "json", - // "max-length": 40, - "on-click": "playerctl play-pause", - "on-click-right": "playerctl stop", - "on-scroll-up": "playerctl next", - "on-scroll-down": "playerctl previous", - "format-icons": { - "spotify": "", - "default": "🎜" - }, - "escape": true, - "smooth-scrolling-threshold": 10, // This value was tested using a trackpad, it should be lowered if using a mouse. - "exec": "$HOME/.config/waybar/scripts/mediaplayer.py 2> /dev/null" // Script in resources folder - // "exec": "$HOME/.config/waybar/scripts/mediaplayer.py --player spotify 2> /dev/null" // Filter player based on name */ - }, - - - "custom/weather": { - "exec": "curl 'https://wttr.in/?format=1'", - "interval": 600, - "on-click": "curl 'https://wttr.in/?format=1'", - }, - - "custom/vpn": { - "format": "VPN ", - "exec": "echo '{\"class\": \"connected\"}'", - "exec-if": "test -d /proc/sys/net/ipv4/conf/tun0", - "return-type": "json", - "interval": 5 - }, - - "custom/pipewire": { - "tooltip": true, - "max-length": 10, - "exec": "$HOME/.config/waybar/scripts/pipewire.sh", - "on-click": "pulsemixer --toggle-mute", - "on-click-right": "qpwgraph" - }, - - "custom/github": { - "format": "{} ", - "return-type": "json", - "interval": 60, - "exec": "$HOME/.config/waybar/scripts/github.sh", - "on-click": "xdg-open https://github.com/notifications" - }, - - "custom/dunst": { - "exec": "~/.config/waybar/scripts/dunst.sh", - "on-click": "dunstctl set-paused toggle", - "restart-interval": 1, - }, -} - diff --git a/config/waybar/config.jsonc b/config/waybar/config.jsonc index 37d5641e..3a2123c5 100644 --- a/config/waybar/config.jsonc +++ b/config/waybar/config.jsonc @@ -12,8 +12,8 @@ "spacing": 0, // Gaps between modules (4px) // Choose the order of the modules "modules-left": [ - // "wlr/taskbar" - "cffi/niri-taskbar" + "wlr/taskbar" + // "cffi/niri-taskbar" ], "modules-center": [], "modules-right": [ @@ -51,7 +51,7 @@ }, "wlr/taskbar": { // "all-outputs": true, - "format": "{title} | {app_id}", + "format": "{app_id}", // "format": "{icon}", "tooltip-format": "{title} | {app_id}", "on-click": "activate", diff --git a/config/yazi/package.toml b/config/yazi/package.toml index 9e5a3cb0..954af899 100644 --- a/config/yazi/package.toml +++ b/config/yazi/package.toml @@ -10,8 +10,8 @@ hash = "e02a788e5b8ae0fb47fd0193dda589cc" [[plugin.deps]] use = "hankertrix/augment-command" -rev = "7c12bdf" -hash = "f7e6d377e4efee567ec6d1c2355f2ca3" +rev = "681158d" +hash = "63ed5325016895306781d13690b246bf" [[plugin.deps]] use = "kirasok/torrent-preview" @@ -20,8 +20,8 @@ hash = "d849ad596b8a77902e62a42403aeba40" [[plugin.deps]] use = "ndtoan96/ouch" -rev = "594b8a2" -hash = "c9e628fc0312d198db22ae2fa74883b" +rev = "406ce6c" +hash = "f5afc904d5106ee368c8aa2ded43bd74" [[plugin.deps]] use = "pirafrank/what-size" @@ -30,38 +30,38 @@ hash = "57056b9728006881d580ccabe8154a9c" [[plugin.deps]] use = "yazi-rs/plugins:git" -rev = "e07bf41" -hash = "270915fa8282a19908449530ff66f7e2" +rev = "1962818" +hash = "26db011a778f261d730d4f5f8bf24b3f" [[plugin.deps]] use = "yazi-rs/plugins:chmod" -rev = "e07bf41" -hash = "8da0b15a97b5dfd13941d1ecc617ac7c" +rev = "1962818" +hash = "f0c8c378184d5f8abd1b095a443d336d" [[plugin.deps]] use = "yazi-rs/plugins:full-border" -rev = "e07bf41" -hash = "3996fc74044bc44144b323686f887e1" +rev = "1962818" +hash = "6fa6a05a81c98dd000fbca3cca6e9682" [[plugin.deps]] use = "yazi-rs/plugins:mount" -rev = "e07bf41" -hash = "563e4068979d1466d3dfc2e70a296947" +rev = "1962818" +hash = "91937a4a9b779eabc6983e258befdfe9" [[plugin.deps]] use = "yazi-rs/plugins:smart-filter" -rev = "e07bf41" -hash = "407d19bc4fb46eff5fa8ff8337644847" +rev = "1962818" +hash = "c887903a63a2ff520081b6d90a4b3392" [[plugin.deps]] use = "yazi-rs/plugins:diff" -rev = "e07bf41" -hash = "2f08b8249b57737e7257298a3b2a2edc" +rev = "1962818" +hash = "8b1af6b5a69797ee951f2a80ce570818" [[plugin.deps]] use = "AnirudhG07/rich-preview" -rev = "573b275" -hash = "c3e2871c9ef244fd181f203791f9b0d2" +rev = "7d616ad" +hash = "d64feec9761392cbc250d199ab4b8a3a" [[plugin.deps]] use = "macydnah/office" @@ -70,8 +70,8 @@ hash = "5805affd3ae8adcb3c72b6997d21c0a6" [[plugin.deps]] use = "boydaihungst/mediainfo" -rev = "dc61636" -hash = "2fa34959353b6f1a1c33659f50e098fd" +rev = "6fbed8d" +hash = "57ae6b43c477e117802f176683b78e74" [[plugin.deps]] use = "iynaix/time-travel" diff --git a/config/yazi/plugins/augment-command.yazi/main.lua b/config/yazi/plugins/augment-command.yazi/main.lua index 0dba7a6b..c3727936 100644 --- a/config/yazi/plugins/augment-command.yazi/main.lua +++ b/config/yazi/plugins/augment-command.yazi/main.lua @@ -452,10 +452,6 @@ local get_mime_type_without_prefix_template_pattern = ---@type string local shell_variable_pattern = "%%[hs]%d?" --- The pattern to match the bat command ----@type string -local bat_command_pattern = "%f[%a]bat%f[%A]" - -- Utility functions -- Function to merge tables. @@ -1415,8 +1411,8 @@ local get_path_of_hovered_item = ya.sync(function(_, quote) -- If there is no hovered item, exit the function if not hovered_item then return end - -- Convert the url of the hovered item to a string - local hovered_item_path = tostring(cx.active.current.hovered.url) + -- Convert the path of the hovered item to a string + local hovered_item_path = tostring(cx.active.current.hovered.url.path) -- If the quote flag is passed, -- then quote the path of the hovered item @@ -1470,8 +1466,8 @@ local get_paths_of_selected_items = ya.sync(function(_, quote) for _, item in pairs(selected_items) do -- - -- Convert the url of the item to a string - local item_path = tostring(item) + -- Convert the path of the item to a string + local item_path = tostring(item.path) -- If the quote flag is passed, -- then quote the path of the item @@ -3481,11 +3477,35 @@ local function handle_create(args, config) return execute_create(full_url, is_directory, args, config) end +-- Function to match a binary name against a search string +---@param binary_name string The name of the binary +---@param search_string string The string to search for the binary name +---@return string binary_pattern The pattern for the binary +---@return string? binary_path The path to the binary +local function match_binary_name(binary_name, search_string) + -- + + -- The binary pattern + local binary_pattern = "%f[%w_%-%.].*" .. binary_name .. "%f[%W%s]" + + -- Get the binary path + local binary_path = search_string:match(binary_pattern) + + -- Escape the binary path if it's not nil + if binary_path ~= nil then + binary_path = escape_replacement_string(binary_path) + end + + -- Return the binary pattern and the path + return binary_pattern, binary_path +end + -- Function to remove the F flag from the less command ---@param command string The shell command containing the less command +---@param less_binary_pattern string The pattern to match the less binary ---@return string command The command with the F flag removed ---@return boolean f_flag_found Whether the F flag was found -local function remove_f_flag_from_less_command(command) +local function remove_f_flag_from_less_command(command, less_binary_pattern) -- -- Initialise the variable to store if the F flag is found @@ -3494,9 +3514,13 @@ local function remove_f_flag_from_less_command(command) -- Initialise the variable to store the replacement count local replacement_count = 0 + -- Initialised the modified command + local modified_command = command + -- Remove the F flag when it is passed at the start -- of the flags given to the less command - command, replacement_count = command:gsub("(%f[%a]less%f[%A].*)%-F", "%1") + modified_command, replacement_count = + modified_command:gsub("(" .. less_binary_pattern .. ".*)%-F", "%1") -- If the replacement count is not 0, -- set the f_flag_found variable to true @@ -3504,27 +3528,36 @@ local function remove_f_flag_from_less_command(command) -- Remove the F flag when it is passed in the middle -- or end of the flags given to the less command command - command, replacement_count = - command:gsub("(%f[%a]less%f[%A].*%-)(%a*)F(%a*)", "%1%2%3") + modified_command, replacement_count = modified_command:gsub( + "(" .. less_binary_pattern .. ".*%-)(%a*)F(%a*)", + "%1%2%3" + ) -- If the replacement count is not 0, -- set the f_flag_found variable to true if replacement_count ~= 0 then f_flag_found = true end -- Return the command and whether or not the F flag was found - return command, f_flag_found + return modified_command, f_flag_found end -- Function to fix a command containing less. -- All this function does is remove -- the F flag from a command containing less. ---@param command string The shell command containing the less command +---@param less_binary_pattern string The pattern to match the less binary +---@param less_binary_path string The path to the less binary ---@return string command The fixed shell command -local function fix_shell_command_containing_less(command) +local function fix_shell_command_containing_less( + command, + less_binary_pattern, + less_binary_path +) -- -- Remove the F flag from the given command - local fixed_command = remove_f_flag_from_less_command(command) + local fixed_command = + remove_f_flag_from_less_command(command, less_binary_pattern) -- Get the LESS environment variable local less_environment_variable = os.getenv("LESS") @@ -3536,7 +3569,10 @@ local function fix_shell_command_containing_less(command) -- Otherwise, remove the F flag from the LESS environment variable -- and check if the F flag was found local less_command_with_modified_env_variables, f_flag_found = - remove_f_flag_from_less_command("less " .. less_environment_variable) + remove_f_flag_from_less_command( + string.format("%s %s", less_binary_path, less_environment_variable), + less_binary_pattern + ) -- If the F flag isn't found, -- then return the given command with the F flag removed @@ -3544,7 +3580,7 @@ local function fix_shell_command_containing_less(command) -- Add the less environment variable flags to the less command fixed_command = fixed_command:gsub( - "%f[%a]less%f[%A]", + less_binary_pattern, escape_replacement_string(less_command_with_modified_env_variables) ) @@ -3557,13 +3593,22 @@ end -- Function to fix the bat default pager command ---@param command string The command containing the bat default pager command +---@param bat_binary_pattern string The pattern to match the bat binary +---@param bat_binary_path string The path to the bat binary ---@return string command The fixed bat command -local function fix_shell_command_containing_bat(command) +local function fix_shell_command_containing_bat( + command, + bat_binary_pattern, + bat_binary_path +) -- -- The pattern to match the pager argument for the bat command local bat_pager_pattern = "(%-%-pager)%s+(%S+)" + -- The default bat pager command without the -F flag + local bat_default_pager_command_without_f_flag = "less -RX" + -- Get the pager argument for the bat command local _, pager_argument = command:match(bat_pager_pattern) @@ -3606,16 +3651,13 @@ local function fix_shell_command_containing_bat(command) return modified_command end - -- If there is no pager argument, - -- initialise the default pager command for bat without the F flag - local bat_default_pager_command_without_f_flag = "less -RX" - - -- Replace the bat command with the command to use the - -- bat default pager command without the F flag + -- Replace the bat command with the command to use + -- the bat default pager command without the F flag local modified_command = command:gsub( - bat_command_pattern, + bat_binary_pattern, string.format( - "bat --pager '%s'", + "%s --pager '%s'", + bat_binary_path, bat_default_pager_command_without_f_flag ), 1 @@ -3631,24 +3673,43 @@ end local function fix_shell_command(command) -- - -- If the given command contains the bat command - if command:find(bat_command_pattern) ~= nil then + -- Get the bat binary pattern and path from the command + local bat_binary_pattern, bat_binary_path = + match_binary_name("bat", command) + + -- Initialise the fixed command + local fixed_command = command + + -- If the bat binary is in the command + if bat_binary_path ~= nil then -- -- Calls the command to fix the bat command - command = fix_shell_command_containing_bat(command) + fixed_command = fix_shell_command_containing_bat( + command, + bat_binary_pattern, + bat_binary_path + ) end - -- If the given command includes the less command - if command:find("%f[%a]less%f[%A]") ~= nil then + -- Get the less binary pattern and path from the fixed command + local less_binary_pattern, less_binary_path = + match_binary_name("less", fixed_command) + + -- If the less binary is in the command + if less_binary_path ~= nil then -- -- Fix the command containing less - command = fix_shell_command_containing_less(command) + fixed_command = fix_shell_command_containing_less( + fixed_command, + less_binary_pattern, + less_binary_path + ) end - -- Return the modified command - return command + -- Return the fixed command + return fixed_command end -- Function to handle a shell command diff --git a/config/yazi/plugins/chmod.yazi/README.md b/config/yazi/plugins/chmod.yazi/README.md index 74616a7d..5a732075 100644 --- a/config/yazi/plugins/chmod.yazi/README.md +++ b/config/yazi/plugins/chmod.yazi/README.md @@ -21,7 +21,7 @@ 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 commands/plugins. +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 diff --git a/config/yazi/plugins/chmod.yazi/main.lua b/config/yazi/plugins/chmod.yazi/main.lua index a4dde679..87af0bfc 100644 --- a/config/yazi/plugins/chmod.yazi/main.lua +++ b/config/yazi/plugins/chmod.yazi/main.lua @@ -1,4 +1,4 @@ ---- @since 25.12.29 +--- @since 26.1.22 local selected_or_hovered = ya.sync(function() local tab, paths = cx.active, {} @@ -37,7 +37,7 @@ return { return end - local output, err = Command("chmod"):arg(value):arg(urls):stderr(Command.PIPED):output() + 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 diff --git a/config/yazi/plugins/diff.yazi/README.md b/config/yazi/plugins/diff.yazi/README.md index 4203a8ca..ea9918fb 100644 --- a/config/yazi/plugins/diff.yazi/README.md +++ b/config/yazi/plugins/diff.yazi/README.md @@ -21,7 +21,7 @@ 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 commands/plugins. +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 diff --git a/config/yazi/plugins/full-border.yazi/main.lua b/config/yazi/plugins/full-border.yazi/main.lua index a917e1e8..5277c19a 100644 --- a/config/yazi/plugins/full-border.yazi/main.lua +++ b/config/yazi/plugins/full-border.yazi/main.lua @@ -20,7 +20,8 @@ local function setup(_, opts) local c = self._chunks self._chunks = { c[1]:pad(ui.Pad.y(1)), - c[2]:pad(ui.Pad(1, c[3].w > 0 and 0 or 1, 1, c[1].w > 0 and 0 or 1)), + -- TODO: remove this compatibility hack + fs.unique and c[2]:pad(ui.Pad.y(1)) or c[2]:pad(ui.Pad(1, c[3].w > 0 and 0 or 1, 1, c[1].w > 0 and 0 or 1)), c[3]:pad(ui.Pad.y(1)), } diff --git a/config/yazi/plugins/git.yazi/main.lua b/config/yazi/plugins/git.yazi/main.lua index 993be7e8..dcb0ce13 100644 --- a/config/yazi/plugins/git.yazi/main.lua +++ b/config/yazi/plugins/git.yazi/main.lua @@ -1,4 +1,4 @@ ---- @since 25.12.29 +--- @since 26.1.22 local WINDOWS = ya.target_family() == "windows" @@ -224,7 +224,6 @@ local function fetch(_, job) :cwd(tostring(cwd)) :arg({ "--no-optional-locks", "-c", "core.quotePath=", "status", "--porcelain", "-unormal", "--no-renames", "--ignored=matching" }) :arg(paths) - :stdout(Command.PIPED) :output() if not output then return true, Err("Cannot spawn `git` command, error: %s", err) diff --git a/config/yazi/plugins/mediainfo.yazi/LICENSE b/config/yazi/plugins/mediainfo.yazi/LICENSE index 0399f1c2..89156806 100644 --- a/config/yazi/plugins/mediainfo.yazi/LICENSE +++ b/config/yazi/plugins/mediainfo.yazi/LICENSE @@ -1,4 +1,5 @@ Copyright (c) 2024 Lauri Niskanen +Copyright (c) 2025 Huy Hoang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/config/yazi/plugins/mediainfo.yazi/README.md b/config/yazi/plugins/mediainfo.yazi/README.md index 7ad6dc9e..11ef1347 100644 --- a/config/yazi/plugins/mediainfo.yazi/README.md +++ b/config/yazi/plugins/mediainfo.yazi/README.md @@ -13,7 +13,8 @@ This is a Yazi plugin for previewing media files. The preview shows thumbnail using `ffmpeg` if available and media metadata using `mediainfo`. > [!IMPORTANT] -> Minimum version: yazi v25.5.31. +> Minimum version: yazi v26.1.22. +> Check it via command `yazi --debug` ## Preview @@ -70,29 +71,48 @@ Config folder for each OS: https://yazi-rs.github.io/docs/configuration/overview Create `.../yazi/yazi.toml` and add: -> [!IMPORTANT] -> -> For yazi (>=v25.12.29) replace `name` with `url` - ```toml [plugin] prepend_preloaders = [ # Replace magick, image, video with mediainfo { mime = "{audio,video,image}/*", run = "mediainfo" }, { mime = "application/subrip", run = "mediainfo" }, - # Adobe Illustrator, Adobe Photoshop is image/adobe.photoshop, already handled above + + # Adobe Photoshop is image/adobe.photoshop, already handled above + # Adobe Illustrator { mime = "application/postscript", run = "mediainfo" }, + { mime = "application/illustrator", run = "mediainfo" }, + { mime = "application/dvb.ait", run = "mediainfo" }, + { mime = "application/vnd.adobe.illustrator", run = "mediainfo" }, + { mime = "image/x-eps", run = "mediainfo" }, + { mime = "application/eps", run = "mediainfo" }, + + # Sometimes AI file is recognized as "application/pdf". Lmao. + # In this case use file extension instead: + { url = "*.{ai,eps,ait}", run = "mediainfo" }, ] prepend_previewers = [ # Replace magick, image, video with mediainfo { mime = "{audio,video,image}/*", run = "mediainfo"}, { mime = "application/subrip", run = "mediainfo" }, - # Adobe Illustrator, Adobe Photoshop is image/adobe.photoshop, already handled above + + # Adobe Photoshop is image/adobe.photoshop, already handled above + # Adobe Illustrator { mime = "application/postscript", run = "mediainfo" }, + { mime = "application/illustrator", run = "mediainfo" }, + { mime = "application/dvb.ait", run = "mediainfo" }, + { mime = "application/vnd.adobe.illustrator", run = "mediainfo" }, + { mime = "image/x-eps", run = "mediainfo" }, + { mime = "application/eps", run = "mediainfo" }, + + # Sometimes AI file is recognized as "application/pdf". Lmao. + # In this case use file extension instead: + { url = "*.{ai,eps,ait}", run = "mediainfo" }, ] - # There are more extensions which are supported by mediainfo. + # There are more extensions, mime types which are supported by mediainfo. # Just add file's MIME type to `previewers`, `preloaders` above. # https://mediaarea.net/en/MediaInfo/Support/Formats + # If it's not working, file an issue at https://github.com/boydaihungst/mediainfo.yazi/issues # For a large file like Adobe Illustrator, Adobe Photoshop, etc # you may need to increase the memory limit if no image is rendered. @@ -104,7 +124,7 @@ Create `.../yazi/yazi.toml` and add: ## Custom theme -Using the same style with spotter. [Read more](https://github.com/sxyazi/yazi/pull/2391) +Using the same style with spotter windows. [Read more](https://github.com/sxyazi/yazi/pull/2391) Edit or add `yazi/theme.toml`: diff --git a/config/yazi/plugins/mediainfo.yazi/adobe.lua b/config/yazi/plugins/mediainfo.yazi/adobe.lua new file mode 100644 index 00000000..90c38c32 --- /dev/null +++ b/config/yazi/plugins/mediainfo.yazi/adobe.lua @@ -0,0 +1,283 @@ +--- @since 26.1.22 + +local M = {} +local const = require(".const") +local utils = require(".utils") + +local function image_layer_count(job) + local cache = ya.file_cache({ file = job.file, skip = 0 }) + if not cache then + return 0 + end + local layer_count = utils.get_state("f" .. tostring(cache)) + if layer_count then + return layer_count + end + local output, err = Command("identify") + :arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) }) + :output() + if err or not output then + return 0 + end + layer_count = 0 + for line in output.stdout:gmatch("[^\r\n]+") do + if line:match("%S") then + layer_count = layer_count + 1 + end + end + utils.set_state("f" .. tostring(cache), layer_count) + return layer_count +end + +function M:peek(job) + local preload_status, preload_err = self:preload(job) + -- Stop if preload failed + if not preload_status then + return + end + + local cache_img_url = ya.file_cache(job) + + local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 }) + + local hide_metadata = utils.get_state(const.STATE_KEY.hide_metadata) + local mediainfo_job_skip = job.skip + ::recalc_mediainfo_job_skip:: + local mediainfo_height = 0 + local lines = {} + local limit = job.area.h + local last_line = 0 + local EOF_mediainfo = true + local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES + + if not hide_metadata then + local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix + local output = utils.read_mediainfo_cached_file(cache_mediainfo_path) + if output then + local max_width = math.max(1, job.area.w) + if output:match("^Error:") then + job.args.force_reload_mediainfo = true + preload_status, preload_err = self:preload(job) + if not preload_status or preload_err then + return + end + output = utils.read_mediainfo_cached_file(cache_mediainfo_path) + end + + output = output:gsub("\n+$", "") + + local iter = output:gmatch("[^\n]*") + local str = iter() + + while str ~= nil do + local next_str = iter() + local label, value = str:match("(.*[^ ]) +: (.*)") + local line + if label then + if not const.skip_labels[label] then + line = ui.Line({ + ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()), + ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")), + }) + end + elseif str ~= "General" then + line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) }) + end + + if line then + local line_height = ui.height + and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap }) + or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1)) + if next_str == nil and line_height == 1 then + EOF_mediainfo = true + end + if (last_line + line_height) > mediainfo_job_skip then + table.insert(lines, line) + end + if (last_line + line_height) >= mediainfo_job_skip + limit then + last_line = mediainfo_job_skip + limit + EOF_mediainfo = false + break + end + last_line = last_line + line_height + end + str = next_str + end + end + mediainfo_height = math.min(limit, last_line) + end + + if not hide_metadata then + if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then + if + image_layer_count(job) + < ( + 1 + + math.floor( + math.max( + 0, + utils.get_state(const.STATE_KEY.units) + and (math.abs(job.skip / utils.get_state(const.STATE_KEY.units))) + or 0 + ) + ) + ) + then + ya.emit("peek", { + math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))), + only_if = job.file.url, + upper_bound = true, + }) + return + else + local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip) + mediainfo_job_skip = last_valid_mediainfo_skip + and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)] + or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0)) + + goto recalc_mediainfo_job_skip + end + else + utils.set_state( + const.STATE_KEY.last_valid_mediainfo_skip, + { [tostring(cache_img_url_no_skip)] = mediainfo_job_skip } + ) + end + end + + -- NOTE: Hacky way to prevent image overlap with old metadata area + if utils.get_state(const.STATE_KEY.prev_metadata_area) then + ya.preview_widget(job, { + ui.Clear(ui.Rect(utils.get_state(const.STATE_KEY.prev_metadata_area))), + }) + end + utils.force_render() + + local rendered_img_rect = cache_img_url + and fs.cha(cache_img_url) + and ya.image_show( + cache_img_url, + ui.Rect({ + x = job.area.x, + y = job.area.y, + w = job.area.w, + h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h, + }) + ) + or nil + local image_height = rendered_img_rect and rendered_img_rect.h or 0 + + -- Handle image preload error + if preload_err then + table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red"))) + end + + ya.preview_widget(job, { + ui.Text(lines) + :area(ui.Rect({ + x = job.area.x, + y = job.area.y + image_height, + w = job.area.w, + h = job.area.h - image_height, + })) + :wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO), + }) + + -- NOTE: Hacky way to prevent image overlap with old metadata area + utils.set_state(const.STATE_KEY.prev_metadata_area, not hide_metadata and { + x = job.area.x, + y = job.area.y + image_height, + w = job.area.w, + h = job.area.h - image_height, + } or nil) +end + +function M:preload(job) + local cmd = "mediainfo" + local err_msg = "" + local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url)) + + -- NOTE: Preload image + + local cache_img_url = ya.file_cache(job) + local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url) + + -- NOTE: Only generate preview image when cache image is not exist + if not cache_img_url_cha or cache_img_url_cha.len <= 0 then + local cache_img_status, image_preload_err + local layer_index = 0 + local units = utils.get_state(const.STATE_KEY.units) + if units ~= nil then + local max_layer = image_layer_count(job) + layer_index = math.floor(math.max(0, math.abs(job.skip / units))) + if layer_index + 1 > max_layer then + layer_index = math.max(0, max_layer - 1) + end + end + local cache_img_url_tmp = Url(cache_img_url .. ".tmp") + if fs.cha(cache_img_url_tmp) then + fs.remove("file", cache_img_url_tmp) + end + local tmp_file_path, _ = type(fs.unique) == "function" and fs.unique("file", cache_img_url_tmp) + or fs.unique_name(cache_img_url_tmp) + cache_img_status, image_preload_err = require("magick") + .with_limit() + :arg({ + "-background", + "none", + tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) .. "[" .. tostring( + layer_index + ) .. "]", + "-auto-orient", + "-strip", + "-resize", + string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height), + "-quality", + rt.preview.image_quality, + string.format("PNG32:%s", tostring(tmp_file_path)), + }) + :status() + if cache_img_status then + os.rename(tostring(tmp_file_path), tostring(cache_img_url)) + end + + if not cache_img_status and image_preload_err then + ya.dbg("mediainfo", image_preload_err) + err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "") + end + end + + -- NOTE: Get mediainfo and save to cache folder + local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix) + local cache_mediainfo_cha = fs.cha(cache_mediainfo_url) + -- Case peek function called preload to refetch mediainfo + if cache_mediainfo_cha and not job.args.force_reload_mediainfo then + return true, err_msg ~= "" and ("Error: " .. err_msg) or nil + end + + local output, err + if is_valid_utf8_path then + output, err = Command(cmd) + :arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) }) + :output() + else + cmd = "cd " + .. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent)) + .. " && " + .. cmd + .. " " + .. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name)) + output, err = Command(const.SHELL):arg({ "-c", cmd }):output() + end + if err then + ya.dbg("mediainfo", tostring(err)) + err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd) + end + + return fs.write( + cache_mediainfo_url, + (err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "") + ) +end + +return M diff --git a/config/yazi/plugins/mediainfo.yazi/audio.lua b/config/yazi/plugins/mediainfo.yazi/audio.lua new file mode 100644 index 00000000..efc90b2d --- /dev/null +++ b/config/yazi/plugins/mediainfo.yazi/audio.lua @@ -0,0 +1,314 @@ +--- @since 26.1.22 + +local M = {} +local const = require(".const") +local utils = require(".utils") + +local function cover_layer_count(job) + local cache = ya.file_cache({ file = job.file, skip = 0 }) + if not cache then + return 0 + end + local layer_count = utils.get_state("f" .. tostring(cache)) + if layer_count then + return layer_count + end + local output, err = Command("ffprobe"):arg({ + "-v", + "error", + "-select_streams", + "v", + "-show_entries", + "stream=index:stream_disposition=attached_pic", + "-of", + "json", + tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url), + }):output() + if err or not output then + return 0 + end + layer_count = 0 + local data = ya.json_decode(output.stdout) + layer_count = #data.streams + utils.set_state("f" .. tostring(cache), layer_count) + return layer_count +end + +function M:peek(job) + local preload_status, preload_err = self:preload(job) + -- Stop if preload failed + if not preload_status then + return + end + + local cache_img_url = ya.file_cache(job) + + local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 }) + + local hide_metadata = utils.get_state(const.STATE_KEY.hide_metadata) + local mediainfo_job_skip = job.skip + ::recalc_mediainfo_job_skip:: + local mediainfo_height = 0 + local lines = {} + local limit = job.area.h + local last_line = 0 + local EOF_mediainfo = true + local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES + + if not hide_metadata then + local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix + local output = utils.read_mediainfo_cached_file(cache_mediainfo_path) + if output then + local max_width = math.max(1, job.area.w) + if output:match("^Error:") then + job.args.force_reload_mediainfo = true + preload_status, preload_err = self:preload(job) + if not preload_status or preload_err then + return + end + output = utils.read_mediainfo_cached_file(cache_mediainfo_path) + end + + output = output:gsub("\n+$", "") + + local iter = output:gmatch("[^\n]*") + local str = iter() + + while str ~= nil do + local next_str = iter() + local label, value = str:match("(.*[^ ]) +: (.*)") + local line + if label then + if not const.skip_labels[label] then + line = ui.Line({ + ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()), + ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")), + }) + end + elseif str ~= "General" then + line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) }) + end + + if line then + local line_height = ui.height + and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap }) + or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1)) + if next_str == nil and line_height == 1 then + EOF_mediainfo = true + end + if (last_line + line_height) > mediainfo_job_skip then + table.insert(lines, line) + end + if (last_line + line_height) >= mediainfo_job_skip + limit then + last_line = mediainfo_job_skip + limit + EOF_mediainfo = false + break + end + last_line = last_line + line_height + end + str = next_str + end + end + mediainfo_height = math.min(limit, last_line) + end + + if not hide_metadata then + if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then + if + cover_layer_count(job) + < ( + 1 + + math.floor( + math.max( + 0, + utils.get_state(const.STATE_KEY.units) + and (math.abs(job.skip / utils.get_state(const.STATE_KEY.units))) + or 0 + ) + ) + ) + then + ya.emit("peek", { + math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))), + only_if = job.file.url, + upper_bound = true, + }) + return + else + -- NOTE: Recalculate mediainfo using cached latest valid skip value when reach the end of mediainfo output + local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip) + mediainfo_job_skip = last_valid_mediainfo_skip + and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)] + or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0)) + + goto recalc_mediainfo_job_skip + end + else + utils.set_state( + const.STATE_KEY.last_valid_mediainfo_skip, + { [tostring(cache_img_url_no_skip)] = mediainfo_job_skip } + ) + end + end + + -- NOTE: Hacky way to prevent image overlap with old metadata area + if utils.get_state(const.STATE_KEY.prev_metadata_area) then + ya.preview_widget(job, { + ui.Clear(ui.Rect(utils.get_state(const.STATE_KEY.prev_metadata_area))), + }) + end + utils.force_render() + + local rendered_img_rect = cache_img_url + and fs.cha(cache_img_url) + and ya.image_show( + cache_img_url, + ui.Rect({ + x = job.area.x, + y = job.area.y, + w = job.area.w, + h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h, + }) + ) + or nil + local image_height = rendered_img_rect and rendered_img_rect.h or 0 + + -- Handle image preload error + if preload_err then + table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red"))) + end + + ya.preview_widget(job, { + ui.Text(lines) + :area(ui.Rect({ + x = job.area.x, + y = job.area.y + image_height, + w = job.area.w, + h = job.area.h - image_height, + })) + :wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO), + }) + + -- NOTE: Hacky way to prevent image overlap with old metadata area + utils.set_state(const.STATE_KEY.prev_metadata_area, not hide_metadata and { + x = job.area.x, + y = job.area.y + image_height, + w = job.area.w, + h = job.area.h - image_height, + } or nil) +end + +function M:preload(job) + local cmd = "mediainfo" + local err_msg = "" + local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url)) + + -- NOTE: Preload image + + local cache_img_url = ya.file_cache(job) + local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url) + + -- NOTE: Only generate preview image when cache image is not exist + if not cache_img_url_cha or cache_img_url_cha.len <= 0 then + local cover_index = 0 + local units = utils.get_state(const.STATE_KEY.units) + if units ~= nil then + local max_layer = cover_layer_count(job) + cover_index = math.floor(math.max(0, math.abs(job.skip / units))) + if cover_index + 1 > max_layer then + cover_index = math.max(0, max_layer - 1) + end + end + local qv = 31 - math.floor(rt.preview.image_quality * 0.3) + local audio_preload_output, audio_preload_err = Command("ffmpeg"):arg({ + "-v", + "error", + "-threads", + 1, + "-i", + tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url), + "-map", + string.format("0:v:%d?", cover_index), + "-an", + "-sn", + "-dn", + "-vframes", + 1, + "-q:v", + qv, + "-vf", + string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height / 2), + "-f", + "image2", + "-y", + tostring(cache_img_url), + }):output() + -- NOTE: Some audio types doesn't have cover image -> error "" + if + ( + audio_preload_output + and audio_preload_output.stderr ~= nil + and audio_preload_output.stderr ~= "" + and not audio_preload_output.stderr:find("Output file does not contain any stream") + ) or audio_preload_err + then + ya.dbg("mediainfo", audio_preload_err) + ya.dbg("mediainfo", audio_preload_output.stderr) + err_msg = err_msg + .. string.format("Failed to start `%s`.\n Do you have `%s` installed?\n", "ffmpeg", "ffmpeg") + else + cache_img_url_cha, _ = fs.cha(cache_img_url) + if not cache_img_url_cha then + -- NOTE: Workaround case audio has no cover image. Prevent regenerate preview image + audio_preload_output, audio_preload_err = require("magick") + .with_limit() + :arg({ + "-size", + "1x1", + "canvas:none", + string.format("PNG32:%s", cache_img_url), + }) + :output() + if (audio_preload_output.stderr ~= nil and audio_preload_output.stderr ~= "") or audio_preload_err then + ya.dbg("mediainfo", image_preload_err) + err_msg = err_msg + .. string.format("Failed to start `%s`.\n Do you have `%s` installed?\n", "magick", "magick") + end + end + end + end + + -- NOTE: Get mediainfo and save to cache folder + local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix) + local cache_mediainfo_cha = fs.cha(cache_mediainfo_url) + -- Case peek function called preload to refetch mediainfo + if cache_mediainfo_cha and not job.args.force_reload_mediainfo then + return true, err_msg ~= "" and ("Error: " .. err_msg) or nil + end + + local output, err + if is_valid_utf8_path then + output, err = Command(cmd) + :arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) }) + :output() + else + cmd = "cd " + .. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent)) + .. " && " + .. cmd + .. " " + .. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name)) + output, err = Command(const.SHELL):arg({ "-c", cmd }):output() + end + if err then + ya.dbg("mediainfo", tostring(err)) + err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd) + end + + return fs.write( + cache_mediainfo_url, + (err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "") + ) +end + +return M diff --git a/config/yazi/plugins/mediainfo.yazi/const.lua b/config/yazi/plugins/mediainfo.yazi/const.lua new file mode 100644 index 00000000..68ef0b23 --- /dev/null +++ b/config/yazi/plugins/mediainfo.yazi/const.lua @@ -0,0 +1,57 @@ +--- @since 26.1.22 + +local M = {} + +M.skip_labels = { + ["Complete name"] = true, + ["CompleteName_Last"] = true, + ["Unique ID"] = true, + ["File size"] = true, + ["Format/Info"] = true, + ["Codec ID/Info"] = true, + ["MD5 of the unencoded content"] = true, +} + +M.ENTRY_ACTION = { + toggle_metadata = "toggle-metadata", +} + +M.STATE_KEY = { + units = "units", + hide_metadata = "hide_metadata", + prev_metadata_area = "prev_metadata_area", + prev_image_height = "prev_image_height", + last_valid_mediainfo_skip = "last_valid_mediainfo_skip", +} + +M.magick_image_mimes = { + avif = true, + hei = true, + heic = true, + heif = true, + ["heif-sequence"] = true, + ["heic-sequence"] = true, + jxl = true, + tiff = true, + xml = true, + -- ["svg+xml"] = true, + ["canon-cr2"] = true, +} + +M.seekable_mimes = { + -- NOTE: Adobe illustrator photoshop mimetypes + ["application/postscript"] = true, + ["application/dvb.ait"] = true, + ["application/illustrator"] = true, + ["application/vnd.adobe.illustrator"] = true, + ["image/x-eps"] = true, + ["application/eps"] = true, + ["application/pdf"] = true, + + ["image/adobe.photoshop"] = true, +} + +M.suffix = "_mediainfo" +M.SHELL = os.getenv("SHELL") or "" + +return M diff --git a/config/yazi/plugins/mediainfo.yazi/image.lua b/config/yazi/plugins/mediainfo.yazi/image.lua new file mode 100644 index 00000000..13dba648 --- /dev/null +++ b/config/yazi/plugins/mediainfo.yazi/image.lua @@ -0,0 +1,234 @@ +--- @since 26.1.22 + +local M = {} +local const = require(".const") +local utils = require(".utils") + +function M:peek(job) + local preload_status, preload_err = self:preload(job) + -- Stop if preload failed + if not preload_status then + return + end + + local cache_img_url = ya.file_cache({ + skip = 0, + args = job.args, + file = job.file, + area = job.area, + }) + + local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 }) + + local hide_metadata = utils.get_state(const.STATE_KEY.hide_metadata) + local mediainfo_height = 0 + local lines = {} + local limit = job.area.h + local last_line = 0 + local EOF_mediainfo = true + local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES + + if not hide_metadata then + local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix + local output = utils.read_mediainfo_cached_file(cache_mediainfo_path) + if output then + local max_width = math.max(1, job.area.w) + if output:match("^Error:") then + job.args.force_reload_mediainfo = true + preload_status, preload_err = self:preload(job) + if not preload_status or preload_err then + return + end + output = utils.read_mediainfo_cached_file(cache_mediainfo_path) + end + + output = output:gsub("\n+$", "") + + local iter = output:gmatch("[^\n]*") + local str = iter() + + while str ~= nil do + local next_str = iter() + local label, value = str:match("(.*[^ ]) +: (.*)") + local line + if label then + if not const.skip_labels[label] then + line = ui.Line({ + ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()), + ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")), + }) + end + elseif str ~= "General" then + line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) }) + end + + if line then + local line_height = ui.height + and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap }) + or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1)) + if next_str == nil and line_height == 1 then + EOF_mediainfo = true + end + if (last_line + line_height) > job.skip then + table.insert(lines, line) + end + if (last_line + line_height) >= job.skip + limit then + last_line = job.skip + limit + EOF_mediainfo = false + break + end + last_line = last_line + line_height + end + str = next_str + end + end + mediainfo_height = math.min(limit, last_line) + end + + if not hide_metadata and EOF_mediainfo and #lines == 0 and job.skip > 0 then + ya.emit("peek", { + math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))), + only_if = job.file.url, + upper_bound = true, + }) + return + end + + utils.force_render() + -- NOTE: Hacky way to prevent image overlap with old metadata area + if utils.get_state(const.STATE_KEY.prev_metadata_area) then + ya.preview_widget(job, { + ui.Clear(ui.Rect(utils.get_state(const.STATE_KEY.prev_metadata_area))), + }) + end + + local rendered_img_rect = cache_img_url + and fs.cha(cache_img_url) + and ya.image_show( + cache_img_url, + ui.Rect({ + x = job.area.x, + y = job.area.y, + w = job.area.w, + h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h, + }) + ) + or nil + local image_height = rendered_img_rect and rendered_img_rect.h or 0 + + -- Handle image preload error + if preload_err then + table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red"))) + end + + ya.preview_widget(job, { + ui.Text(lines) + :area(ui.Rect({ + x = job.area.x, + y = job.area.y + image_height, + w = job.area.w, + h = job.area.h - image_height, + })) + :wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO), + }) + + -- NOTE: Hacky way to prevent image overlap with old metadata area + utils.set_state(const.STATE_KEY.prev_metadata_area, not hide_metadata and { + x = job.area.x, + y = job.area.y + image_height, + w = job.area.w, + h = job.area.h - image_height, + } or nil) +end + +function M:preload(job) + local cmd = "mediainfo" + local err_msg = "" + local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url)) + + -- NOTE: Preload image + + local mime = job.mime:match(".*/(.*)$") + local is_svg = mime == "svg+xml" + local is_magick = const.magick_image_mimes[mime] + local no_skip_job = { skip = 0, file = job.file, args = job.args, area = job.area } + local cache_img_url = ya.file_cache(no_skip_job) + local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url) + + -- NOTE: Only generate preview image when cache image is not exist + if not cache_img_url_cha or cache_img_url_cha.len <= 0 then + local cache_img_status, image_preload_err + if not is_valid_utf8_path then + -- NOTE: Case not valid utf8 path, use trick to generate preview image + if is_svg then + local cache_img_url_tmp = Url(cache_img_url .. ".tmp") + if fs.cha(cache_img_url_tmp) then + fs.remove("file", cache_img_url_tmp) + end + local tmp_file_path, _ = type(fs.unique) == "function" and fs.unique("file", cache_img_url_tmp) + or fs.unique_name(cache_img_url_tmp) + -- svg under invalid utf8 path + cache_img_status, image_preload_err = require("magick") + .with_limit() + :arg({ + "-background", + "none", + tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url), + "-auto-orient", + "-strip", + string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height), + "-quality", + rt.preview.image_quality, + string.format("PNG32:%s", tostring(tmp_file_path)), + }) + :status() + if cache_img_status then + os.rename(tostring(tmp_file_path), tostring(cache_img_url)) + end + end + else + -- NOTE: Case valid utf8 path, use image, svg, or magick module + local image_module = is_svg and "svg" or (is_magick and "magick" or "image") + cache_img_status, image_preload_err = require(image_module):preload(no_skip_job) + end + + if not cache_img_status and image_preload_err then + ya.dbg("mediainfo", image_preload_err) + err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "") + end + end + + -- NOTE: Get mediainfo and save to cache folder + local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix) + local cache_mediainfo_cha = fs.cha(cache_mediainfo_url) + -- Case peek function called preload to refetch mediainfo + if cache_mediainfo_cha and not job.args.force_reload_mediainfo then + return true, err_msg ~= "" and ("Error: " .. err_msg) or nil + end + + local output, err + if is_valid_utf8_path then + output, err = Command(cmd) + :arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) }) + :output() + else + cmd = "cd " + .. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent)) + .. " && " + .. cmd + .. " " + .. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name)) + output, err = Command(const.SHELL):arg({ "-c", cmd }):output() + end + if err then + ya.dbg("mediainfo", tostring(err)) + err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd) + end + + return fs.write( + cache_mediainfo_url, + (err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "") + ) +end + +return M diff --git a/config/yazi/plugins/mediainfo.yazi/main.lua b/config/yazi/plugins/mediainfo.yazi/main.lua index cde53cab..ed0d4a64 100644 --- a/config/yazi/plugins/mediainfo.yazi/main.lua +++ b/config/yazi/plugins/mediainfo.yazi/main.lua @@ -1,472 +1,79 @@ ---- @since 25.5.31 - -local skip_labels = { - ["Complete name"] = true, - ["CompleteName_Last"] = true, - ["Unique ID"] = true, - ["File size"] = true, - ["Format/Info"] = true, - ["Codec ID/Info"] = true, - ["MD5 of the unencoded content"] = true, -} - -local ENTRY_ACTION = { - toggle_metadata = "toggle-metadata", -} - -local STATE_KEY = { - units = "units", - hide_metadata = "hide_metadata", - prev_metadata_area = "prev_metadata_area", -} - -local magick_image_mimes = { - avif = true, - hei = true, - heic = true, - heif = true, - ["heif-sequence"] = true, - ["heic-sequence"] = true, - jxl = true, - tiff = true, - xml = true, - ["svg+xml"] = true, - ["canon-cr2"] = true, -} - -local seekable_mimes = { - ["application/postscript"] = true, - ["image/adobe.photoshop"] = true, -} +--- @since 26.1.22 local M = {} -local suffix = "_mediainfo" -local SHELL = os.getenv("SHELL") or "" - -local function is_valid_utf8(str) - return utf8.len(str) ~= nil -end - -local function path_quote(path) - if not path or tostring(path) == "" then - return path - end - local result = "'" .. string.gsub(tostring(path), "'", "'\\''") .. "'" - return result -end - -local function read_mediainfo_cached_file(file_path) - -- Open the file in read mode - local file = io.open(file_path, "r") - - if file then - -- Read the entire file content - local content = file:read("*all") - file:close() - return content - 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 force_render = ya.sync(function(_, _) - (ui.render or ya.render)() -end) - -local function image_layer_count(job) - local cache = ya.file_cache({ file = job.file, skip = 0 }) - if not cache then - return 0 - end - local layer_count = get_state("f" .. tostring(cache)) - if layer_count then - return layer_count - end - local output, err = Command("identify") - :arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) }) - :output() - if err then - return 0 - end - layer_count = 0 - for line in output.stdout:gmatch("[^\r\n]+") do - if line:match("%S") then - layer_count = layer_count + 1 - end - end - set_state("f" .. tostring(cache), layer_count) - return layer_count -end +local const = require(".const") +local utils = require(".utils") +local adobe = require(".adobe") +local audio = require(".audio") +local image = require(".image") +local video = require(".video") function M:peek(job) + -- debounce peek local start = os.clock() - local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 }) + ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock())) + + -- Need mime to decide which module to use if not job.mime then return end + local is_video = string.find(job.mime, "^video/") local is_audio = string.find(job.mime, "^audio/") local is_image = string.find(job.mime, "^image/") - local is_seekable = seekable_mimes[job.mime] or is_video - local cache_img_url = (is_audio or is_image) and cache_img_url_no_skip + local is_adobe = const.seekable_mimes[job.mime] - if is_seekable then - cache_img_url = ya.file_cache(job) + if is_adobe then + return adobe:peek(job) + elseif is_image then + return image:peek(job) + elseif is_video then + return video:peek(job) + elseif is_audio then + return audio:peek(job) end - local preload_status, preload_err = self:preload(job) - if not preload_status then - return - end - ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock())) - local hide_metadata = get_state(STATE_KEY.hide_metadata) - local mediainfo_height = 0 - local lines = {} - local limit = job.area.h - local last_line = 0 - local is_wrap = rt.preview.wrap == "yes" - if not hide_metadata then - local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. suffix - local output = read_mediainfo_cached_file(cache_mediainfo_path) - if output then - local max_width = math.max(1, job.area.w) - if output:match("^Error:") then - job.args.force_reload_mediainfo = true - preload_status, preload_err = self:preload(job) - if not preload_status or preload_err then - return - end - output = read_mediainfo_cached_file(cache_mediainfo_path) - end - - for str in output:gsub("\n+$", ""):gmatch("[^\n]*") do - local label, value = str:match("(.*[^ ]) +: (.*)") - local line - if label then - if not skip_labels[label] then - line = ui.Line({ - ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()), - ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")), - }) - end - elseif str ~= "General" then - line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) }) - end - - if line then - local line_height = math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1) - if (last_line + line_height) > job.skip then - table.insert(lines, line) - end - if (last_line + line_height) >= job.skip + limit then - last_line = job.skip + limit - break - end - last_line = last_line + line_height - end - end - end - mediainfo_height = math.min(limit, last_line) - end - - if - (job.skip > 0 and #lines == 0 and not hide_metadata) - and ( - not is_seekable - or (is_video and job.skip >= 90) - or ( - (job.mime == "image/adobe.photoshop" or job.mime == "application/postscript") - and image_layer_count(job) - < (1 + math.floor( - math.max(0, get_state(STATE_KEY.units) and (job.skip / get_state(STATE_KEY.units)) or 0) - )) - ) - ) - then - ya.emit("peek", { - math.max(0, job.skip - (get_state(STATE_KEY.units) or limit)), - only_if = job.file.url, - upper_bound = true, - }) - return - end - force_render() - -- NOTE: Hacky way to prevent image overlap with old metadata area - if hide_metadata and get_state(STATE_KEY.prev_metadata_area) then - ya.preview_widget(job, { - ui.Clear(ui.Rect(get_state(STATE_KEY.prev_metadata_area))), - }) - ya.sleep(0.1) - end - local rendered_img_rect = cache_img_url - and fs.cha(cache_img_url) - and ya.image_show( - cache_img_url, - ui.Rect({ - x = job.area.x, - y = job.area.y, - w = job.area.w, - h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h, - }) - ) - or nil - local image_height = rendered_img_rect and rendered_img_rect.h or 0 - - -- NOTE: Workaround case audio has no cover image. Prevent regenerate preview image - if is_audio and image_height == 1 then - local info = ya.image_info(cache_img_url) - if not info or (info.w == 1 and info.h == 1) then - image_height = 0 - end - end - - -- NOTE: Workaround case video.lua doesn't doesn't generate preview image because of `skip` overflow video duration - if is_video and not rendered_img_rect then - image_height = math.max(job.area.h - mediainfo_height, 0) - end - - -- Handle image preload error - if preload_err then - table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red"))) - end - - ya.preview_widget(job, { - ui.Text(lines) - :area(ui.Rect({ - x = job.area.x, - y = job.area.y + image_height, - w = job.area.w, - h = job.area.h - image_height, - })) - :wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO), - }) - -- NOTE: Hacky way to prevent image overlap with old metadata area - set_state(STATE_KEY.prev_metadata_area, not hide_metadata and { - x = job.area.x, - y = job.area.y + image_height, - w = job.area.w, - h = job.area.h - image_height, - } or nil) end function M:seek(job) local h = cx.active.current.hovered if h and h.url == job.file.url then - set_state(STATE_KEY.units, job.units) + utils.set_state(const.STATE_KEY.units, job.units) ya.emit("peek", { math.max(0, cx.active.preview.skip + job.units), only_if = job.file.url, }) end end - function M:preload(job) local cache_img_url = ya.file_cache({ file = job.file, skip = 0 }) if not cache_img_url then + ya.dbg("mediainfo", "Can't access yazi cache folder") return true end - local cache_mediainfo_url = Url(tostring(cache_img_url) .. suffix) - cache_img_url = seekable_mimes[job.mime] and ya.file_cache(job) or cache_img_url - local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url) - local err_msg = "" - local is_valid_utf8_path = is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url)) - -- video mimetype - if job.mime then - if string.find(job.mime, "^video/") then - local cache_img_status, video_preload_err = require("video"):preload(job) - if not cache_img_status and video_preload_err then - err_msg = err_msg - .. string.format("Failed to start `%s`, Do you have `%s` installed?\n", "ffmpeg", "ffmpeg") - end - -- audo and image mimetype - elseif cache_img_url and (not cache_img_url_cha or cache_img_url_cha.len <= 0) then - -- audio - if string.find(job.mime, "^audio/") then - local qv = 31 - math.floor(rt.preview.image_quality * 0.3) - local audio_preload_output, audio_preload_err = Command("ffmpeg"):arg({ - "-v", - "error", - "-threads", - 1, - "-an", - "-sn", - "-dn", - "-i", - tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url), - "-vframes", - 1, - "-q:v", - qv, - "-vf", - string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height / 2), - "-f", - "image2", - "-y", - tostring(cache_img_url), - }):output() - -- NOTE: Some audio types doesn't have cover image -> error "" - if - ( - audio_preload_output - and audio_preload_output.stderr ~= nil - and audio_preload_output.stderr ~= "" - and not audio_preload_output.stderr:find("Output file does not contain any stream") - ) or audio_preload_err - then - err_msg = err_msg - .. string.format("Failed to start `%s`, Do you have `%s` installed?\n", "ffmpeg", "ffmpeg") - else - cache_img_url_cha, _ = fs.cha(cache_img_url) - if not cache_img_url_cha then - -- NOTE: Workaround case audio has no cover image. Prevent regenerate preview image - audio_preload_output, audio_preload_err = require("magick") - .with_limit() - :arg({ - "-size", - "1x1", - "canvas:none", - string.format("PNG32:%s", cache_img_url), - }) - :output() - if - (audio_preload_output.stderr ~= nil and audio_preload_output.stderr ~= "") - or audio_preload_err - then - err_msg = err_msg - .. string.format( - "Failed to start `%s`, Do you have `%s` installed?\n", - "magick", - "magick" - ) - end - end - end - -- image - elseif string.find(job.mime, "^image/") or job.mime == "application/postscript" then - local svg_plugin_ok, svg_plugin = pcall(require, "svg") - local magick_plugin_ok, magick_plugin = pcall(require, "magick") - local mime = job.mime:match(".*/(.*)$") + if not job.mime then + return false + end + local is_video = string.find(job.mime, "^video/") + local is_audio = string.find(job.mime, "^audio/") + local is_image = string.find(job.mime, "^image/") + local is_adobe = const.seekable_mimes[job.mime] - local image_plugin = magick_image_mimes[mime] - and ((mime == "svg+xml" and svg_plugin_ok) and svg_plugin or (magick_plugin_ok and magick_plugin)) - or require("image") - - local cache_img_status, image_preload_err - -- psd, ai, eps - if mime == "adobe.photoshop" or job.mime == "application/postscript" then - local layer_index = 0 - local units = get_state(STATE_KEY.units) - if units ~= nil then - local max_layer = image_layer_count(job) - layer_index = math.floor(math.max(0, job.skip / units)) - if layer_index + 1 > max_layer then - layer_index = max_layer - 1 - end - end - local cache_img_url_tmp = Url(cache_img_url .. ".tmp") - if fs.cha(cache_img_url_tmp) then - fs.remove("file", cache_img_url_tmp) - end - local tmp_file_path, _ = fs.unique_name(cache_img_url_tmp) - cache_img_status, image_preload_err = magick_plugin - .with_limit() - :arg({ - "-background", - "none", - tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) - .. "[" - .. tostring(layer_index) - .. "]", - "-auto-orient", - "-strip", - "-resize", - string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height), - "-quality", - rt.preview.image_quality, - string.format("PNG32:%s", tostring(tmp_file_path)), - }) - :status() - if cache_img_status then - os.rename(tostring(tmp_file_path), tostring(cache_img_url)) - end - elseif mime == "svg+xml" and not is_valid_utf8_path then - local cache_img_url_tmp = Url(cache_img_url .. ".tmp") - if fs.cha(cache_img_url_tmp) then - fs.remove("file", cache_img_url_tmp) - end - local tmp_file_path, _ = fs.unique_name(cache_img_url_tmp) - -- svg under invalid utf8 path - cache_img_status, image_preload_err = magick_plugin - .with_limit() - :arg({ - "-background", - "none", - tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url), - "-auto-orient", - "-strip", - "-flatten", - "-resize", - string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height), - "-quality", - rt.preview.image_quality, - string.format("PNG32:%s", tostring(tmp_file_path)), - }) - :status() - if cache_img_status then - os.rename(tostring(tmp_file_path), tostring(cache_img_url)) - end - else - -- other image - local no_skip_job = { skip = 0, file = job.file, args = {} } - cache_img_status, image_preload_err = image_plugin:preload(no_skip_job) - end - if not cache_img_status then - err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "") - end - end - end + if is_adobe then + return adobe:preload(job) + elseif is_image then + return image:preload(job) + elseif is_video then + return video:preload(job) + elseif is_audio then + return audio:preload(job) end - local cache_mediainfo_cha = fs.cha(cache_mediainfo_url) - if cache_mediainfo_cha and not job.args.force_reload_mediainfo then - return true, err_msg ~= "" and ("Error: " .. err_msg) or nil - end - local cmd = "mediainfo" - local output, err - if is_valid_utf8_path then - output, err = Command(cmd) - :arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) }) - :output() - else - cmd = "cd " - .. path_quote(job.file.path or job.file.cache or (job.file.url.path or job.file.url).parent) - .. " && " - .. cmd - .. " " - .. path_quote(tostring(job.file.path or job.file.cache or job.file.url.name)) - output, err = Command(SHELL) - :arg({ "-c", cmd }) - :arg({ tostring(job.file.path or job.file.cache or (job.file.url.path or job.file.url)) }) - :output() - end - if err then - err_msg = err_msg .. string.format("Failed to start `%s`, Do you have `%s` installed?\n", cmd, cmd) - end - return fs.write( - cache_mediainfo_url, - (err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "") - ) end function M:entry(job) local action = job.args[1] - if action == ENTRY_ACTION.toggle_metadata then - set_state(STATE_KEY.hide_metadata, not get_state(STATE_KEY.hide_metadata)) + if action == const.ENTRY_ACTION.toggle_metadata then + utils.set_state(const.STATE_KEY.hide_metadata, not utils.get_state(const.STATE_KEY.hide_metadata)) ya.emit("peek", { force = true, }) diff --git a/config/yazi/plugins/mediainfo.yazi/utils.lua b/config/yazi/plugins/mediainfo.yazi/utils.lua new file mode 100644 index 00000000..72787d5c --- /dev/null +++ b/config/yazi/plugins/mediainfo.yazi/utils.lua @@ -0,0 +1,41 @@ +--- @since 26.1.22 + +local M = {} + +function M.is_valid_utf8(str) + return utf8.len(str) ~= nil +end + +function M.path_quote(path) + if not path or tostring(path) == "" then + return path + end + local result = "'" .. string.gsub(tostring(path), "'", "'\\''") .. "'" + return result +end + +function M.read_mediainfo_cached_file(file_path) + -- Open the file in read mode + local file = io.open(file_path, "r") + + if file then + -- Read the entire file content + local content = file:read("*all") + file:close() + return content + end +end + +M.force_render = ya.sync(function(_, _) + (ui.render or ya.render)() +end) + +M.set_state = ya.sync(function(state, key, value) + state[key] = value +end) + +M.get_state = ya.sync(function(state, key) + return state[key] +end) + +return M diff --git a/config/yazi/plugins/mediainfo.yazi/video.lua b/config/yazi/plugins/mediainfo.yazi/video.lua new file mode 100644 index 00000000..da7836af --- /dev/null +++ b/config/yazi/plugins/mediainfo.yazi/video.lua @@ -0,0 +1,222 @@ +--- @since 26.1.22 + +local M = {} +local const = require(".const") +local utils = require(".utils") + +function M:peek(job) + local preload_status, preload_err = self:preload(job) + -- Stop if preload failed + if not preload_status then + return + end + + local cache_img_url = ya.file_cache({ + skip = job.skip > 90 and 90 or job.skip, + args = job.args, + file = job.file, + area = job.area, + }) + local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 }) + + local hide_metadata = utils.get_state(const.STATE_KEY.hide_metadata) + local mediainfo_job_skip = job.skip + ::recalc_mediainfo_job_skip:: + local mediainfo_height = 0 + local lines = {} + local limit = job.area.h + local last_line = 0 + local EOF_mediainfo = true + local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES + + if not hide_metadata then + local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix + local output = utils.read_mediainfo_cached_file(cache_mediainfo_path) + if output then + local max_width = math.max(1, job.area.w) + if output:match("^Error:") then + job.args.force_reload_mediainfo = true + preload_status, preload_err = self:preload(job) + if not preload_status or preload_err then + return + end + output = utils.read_mediainfo_cached_file(cache_mediainfo_path) + end + + output = output:gsub("\n+$", "") + + local iter = output:gmatch("[^\n]*") + local str = iter() + + while str ~= nil do + local next_str = iter() + local label, value = str:match("(.*[^ ]) +: (.*)") + local line + if label then + if not const.skip_labels[label] then + line = ui.Line({ + ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()), + ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")), + }) + end + elseif str ~= "General" then + line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) }) + end + + if line then + local line_height = ui.height + and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap }) + or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1)) + if next_str == nil and line_height == 1 then + EOF_mediainfo = true + end + if (last_line + line_height) > mediainfo_job_skip then + table.insert(lines, line) + end + if (last_line + line_height) >= mediainfo_job_skip + limit then + last_line = mediainfo_job_skip + limit + EOF_mediainfo = false + break + end + last_line = last_line + line_height + end + str = next_str + end + end + mediainfo_height = math.min(limit, last_line) + end + + if not hide_metadata then + if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then + if job.skip > 90 then + ya.emit("peek", { + math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))), + only_if = job.file.url, + upper_bound = true, + }) + return + else + -- NOTE: Recalculate mediainfo using cached latest valid skip value when reach the end of mediainfo output + local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip) + mediainfo_job_skip = last_valid_mediainfo_skip + and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)] + or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0)) + + goto recalc_mediainfo_job_skip + end + else + utils.set_state( + const.STATE_KEY.last_valid_mediainfo_skip, + { [tostring(cache_img_url_no_skip)] = mediainfo_job_skip } + ) + end + end + + -- NOTE: Hacky way to prevent image overlap with old metadata area + if utils.get_state(const.STATE_KEY.prev_metadata_area) then + ya.preview_widget(job, { + ui.Clear(ui.Rect(utils.get_state(const.STATE_KEY.prev_metadata_area))), + }) + end + + utils.force_render() + local rendered_img_rect = cache_img_url + and fs.cha(cache_img_url) + and ya.image_show( + cache_img_url, + ui.Rect({ + x = job.area.x, + y = job.area.y, + w = job.area.w, + h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h, + }) + ) + or nil + local image_height = rendered_img_rect and rendered_img_rect.h or 0 + + -- NOTE: Workaround case video.lua doesn't doesn't generate preview image because of `skip` overflow video duration + if not rendered_img_rect then + local prev_image_height = utils.get_state(const.STATE_KEY.prev_image_height) + image_height = prev_image_height and prev_image_height[tostring(cache_img_url_no_skip)] or 0 + else + utils.set_state(const.STATE_KEY.prev_image_height, { [tostring(cache_img_url_no_skip)] = image_height }) + end + + -- Handle image preload error + if preload_err then + table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red"))) + end + + ya.preview_widget(job, { + ui.Text(lines) + :area(ui.Rect({ + x = job.area.x, + y = job.area.y + image_height, + w = job.area.w, + h = job.area.h - image_height, + })) + :wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO), + }) + + -- NOTE: Hacky way to prevent image overlap with old metadata area + utils.set_state(const.STATE_KEY.prev_metadata_area, not hide_metadata and { + x = job.area.x, + y = job.area.y + image_height, + w = job.area.w, + h = job.area.h - image_height, + } or nil) +end + +function M:preload(job) + local cmd = "mediainfo" + local err_msg = "" + + -- NOTE: Preload image from video + + local cache_img_status, video_preload_err = require("video"):preload({ + skip = job.skip > 90 and 90 or job.skip, + args = job.args, + file = job.file, + area = job.area, + }) + + if not cache_img_status and video_preload_err then + ya.dbg("mediainfo", video_preload_err) + err_msg = err_msg .. string.format("Failed to start `%s`.\n Do you have `%s` installed?\n", "ffmpeg", "ffmpeg") + end + + -- NOTE: Get mediainfo and save to cache folder + local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix) + local cache_mediainfo_cha = fs.cha(cache_mediainfo_url) + -- Case peek function called preload to refetch mediainfo + if cache_mediainfo_cha and not job.args.force_reload_mediainfo then + return true, err_msg ~= "" and ("Error: " .. err_msg) or nil + end + + local output, err + local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url)) + if is_valid_utf8_path then + output, err = Command(cmd) + :arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) }) + :output() + else + cmd = "cd " + .. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent)) + .. " && " + .. cmd + .. " " + .. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name)) + output, err = Command(const.SHELL):arg({ "-c", cmd }):output() + end + if err then + ya.dbg("mediainfo", tostring(err)) + err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd) + end + + return fs.write( + cache_mediainfo_url, + (err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "") + ) +end + +return M diff --git a/config/yazi/plugins/mount.yazi/README.md b/config/yazi/plugins/mount.yazi/README.md index 20a56151..16b5a16d 100644 --- a/config/yazi/plugins/mount.yazi/README.md +++ b/config/yazi/plugins/mount.yazi/README.md @@ -25,7 +25,7 @@ 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 commands/plugins. +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 diff --git a/config/yazi/plugins/mount.yazi/cross.lua b/config/yazi/plugins/mount.yazi/cross.lua new file mode 100644 index 00000000..85c14f79 --- /dev/null +++ b/config/yazi/plugins/mount.yazi/cross.lua @@ -0,0 +1,66 @@ +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 + return require(".sudo").run_with_sudo("udisksctl", args) + 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 index 2aef4145..cd375dea 100644 --- a/config/yazi/plugins/mount.yazi/main.lua +++ b/config/yazi/plugins/mount.yazi/main.lua @@ -129,11 +129,11 @@ function M:entry(job) if run == "quit" then break elseif run == "mount" then - self.operate("mount") + require(".cross").operate("mount", active_partition()) elseif run == "unmount" then - self.operate("unmount") + require(".cross").operate("unmount", active_partition()) elseif run == "eject" then - self.operate("eject") + require(".cross").operate("eject", active_partition()) end until not run end @@ -249,48 +249,6 @@ function M.fillin(tbl) return tbl end -function M.operate(type) - local active = active_partition() - if not active then - return - elseif not active.sub then - return -- TODO: mount/unmount main disk - end - - local cmd - if ya.target_os() == "macos" then - cmd = Command("diskutil"):arg { type, active.src } - end - if ya.target_os() == "linux" then - if type == "eject" and active.src:match("^/dev/sr%d+") then - Command("udisksctl"):arg({ "unmount", "-b", active.src }):status() - cmd = Command("eject"):arg { "--traytoggle", active.src } - elseif type == "eject" then - Command("udisksctl"):arg({ "unmount", "-b", active.src }):status() - cmd = Command("udisksctl"):arg { "power-off", "-b", active.src } - else - cmd = Command("udisksctl"):arg { type, "-b", active.src } - end - end - - if not cmd then - return M.fail("mount.yazi is not currently supported on your platform") - end - - local output, err = cmd:output() - if not output then - if cmd.program then - M.fail("Failed to spawn `%s`: %s", cmd.program, err) - else - M.fail("Failed to spawn `udisksctl`: %s", err) -- TODO: remove - end - elseif not output.status.success then - M.fail("Failed to %s `%s`: %s", type, active.src, output.stderr) - end -end - -function M.fail(...) ya.notify { title = "Mount", content = string.format(...), timeout = 10, level = "error" } end - function M:click() end function M:scroll() end diff --git a/config/yazi/plugins/mount.yazi/sudo.lua b/config/yazi/plugins/mount.yazi/sudo.lua new file mode 100644 index 00000000..b157e6d3 --- /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/ouch.yazi/README.md b/config/yazi/plugins/ouch.yazi/README.md index d8c2b88e..efb59697 100644 --- a/config/yazi/plugins/ouch.yazi/README.md +++ b/config/yazi/plugins/ouch.yazi/README.md @@ -1,3 +1,35 @@ + + + # ouch.yazi [ouch](https://github.com/ouch-org/ouch) plugin for [Yazi](https://github.com/sxyazi/yazi). diff --git a/config/yazi/plugins/rich-preview.yazi/main.lua b/config/yazi/plugins/rich-preview.yazi/main.lua index a75003f0..e5518b3c 100644 --- a/config/yazi/plugins/rich-preview.yazi/main.lua +++ b/config/yazi/plugins/rich-preview.yazi/main.lua @@ -2,6 +2,7 @@ local M = {} function M:peek(job) local child = Command("rich") + :env("COLUMNS", tostring(job.area.w)) :arg({ "-j", "--left", diff --git a/config/yazi/plugins/smart-filter.yazi/README.md b/config/yazi/plugins/smart-filter.yazi/README.md index 31872e6a..29653394 100644 --- a/config/yazi/plugins/smart-filter.yazi/README.md +++ b/config/yazi/plugins/smart-filter.yazi/README.md @@ -21,7 +21,7 @@ run = "plugin smart-filter" desc = "Smart filter" ``` -Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other commands/plugins. +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