Update 2026-03-13

This commit is contained in:
2026-03-13 14:57:50 +02:00
parent b97f7aaf4a
commit 75f8df8582
35 changed files with 2158 additions and 1134 deletions

View File

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

1
config/AGENTS-CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
AGENTS.md

View File

@@ -891,7 +891,7 @@
"authenticated": true,
"url": "https://api.vencord.dev/",
"settingsSync": true,
"settingsSyncVersion": 1770991316025
"settingsSyncVersion": 1773063985102
},
"enabledThemes": [],
"eagerPatches": false,

View File

@@ -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" ]

View File

@@ -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):

View File

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

View File

@@ -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 "<width>x<height>" or "<width>x<height>@<refresh rate>".
// 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
}

View File

@@ -30,6 +30,8 @@
<string>comment</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#797593</string>
</dict>
@@ -38,7 +40,7 @@
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string</string>
<string>string, punctuation.definition.string</string>
<key>settings</key>
<dict>
<key>foreground</key>
@@ -53,7 +55,7 @@
<key>settings</key>
<dict>
<key>foreground</key>
<string>#907aa9</string>
<string>#ea9d34</string>
</dict>
</dict>
<dict>
@@ -63,8 +65,10 @@
<string>constant.language</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#907aa9</string>
<string>#ea9d34</string>
</dict>
</dict>
<dict>
@@ -75,7 +79,7 @@
<key>settings</key>
<dict>
<key>foreground</key>
<string>#907aa9</string>
<string>#ea9d34</string>
</dict>
</dict>
<dict>
@@ -86,9 +90,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>italic</string>
<key>foreground</key>
<string>#d7827e</string>
<string>#575279</string>
</dict>
</dict>
<dict>
@@ -99,7 +103,7 @@
<key>settings</key>
<dict>
<key>foreground</key>
<string>#b4637a</string>
<string>#286983</string>
</dict>
</dict>
<dict>
@@ -112,7 +116,7 @@
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#b4637a</string>
<string>#56949f</string>
</dict>
</dict>
<dict>
@@ -123,7 +127,7 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<string></string>
<key>foreground</key>
<string>#56949f</string>
</dict>
@@ -136,7 +140,7 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string> bold</string>
<string>bold</string>
<key>foreground</key>
<string>#286983</string>
</dict>
@@ -162,9 +166,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>italic</string>
<key>foreground</key>
<string>#286983</string>
<string>#d7827e</string>
</dict>
</dict>
<dict>
@@ -175,9 +179,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<string></string>
<key>foreground</key>
<string>#ea9d34</string>
<string>#907aa9</string>
</dict>
</dict>
<dict>
@@ -188,9 +192,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#b4637a</string>
<string>#286983</string>
</dict>
</dict>
<dict>
@@ -203,7 +207,7 @@
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#286983</string>
<string>#907aa9</string>
</dict>
</dict>
<dict>
@@ -214,9 +218,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#56949f</string>
<string>#d7827e</string>
</dict>
</dict>
<dict>
@@ -227,9 +231,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#56949f</string>
<string>#ea9d34</string>
</dict>
</dict>
<dict>
@@ -240,7 +244,7 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<string>bold</string>
<key>foreground</key>
<string>#56949f</string>
</dict>
@@ -253,7 +257,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#b4637a</string>
</dict>
</dict>
<dict>
@@ -284,16 +290,27 @@
<string>#575279</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation, Operators</string>
<key>scope</key>
<string>punctuation, keyword.operator</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#797593</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>dac29768-bcff-4df3-936c-88c7540d550d</string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>semanticClass</key>
<string>theme.light.rosé_pine-dawn</string>
<string>theme.light.rose-pine-dawn</string>
<key>author</key>
<string>oplik0</string>
<string>arrrgi</string>
<key>comment</key>
<string>soho vibes - modified from the sublime text theme by ThatOneCalculator</string>
<string>All natural pine, faux fur and a bit of soho vibes for the classy minimalist</string>
<key>uuid</key>
<string>BB4B4616-E742-41D5-BB5B-63D45FA614F</string>
</dict>
</plist>

View File

@@ -20,7 +20,7 @@
<key>lineHighlight</key>
<string>#2a283e</string>
<key>selection</key>
<string>#6e6a86</string>
<string>#44415a</string>
</dict>
</dict>
<dict>
@@ -30,6 +30,8 @@
<string>comment</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#908caa</string>
</dict>
@@ -38,7 +40,7 @@
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string</string>
<string>string, punctuation.definition.string</string>
<key>settings</key>
<dict>
<key>foreground</key>
@@ -53,7 +55,7 @@
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c4a7e7</string>
<string>#f6c177</string>
</dict>
</dict>
<dict>
@@ -63,8 +65,10 @@
<string>constant.language</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#c4a7e7</string>
<string>#f6c177</string>
</dict>
</dict>
<dict>
@@ -75,7 +79,7 @@
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c4a7e7</string>
<string>#f6c177</string>
</dict>
</dict>
<dict>
@@ -86,9 +90,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>italic</string>
<key>foreground</key>
<string>#ea9a97</string>
<string>#e0def4</string>
</dict>
</dict>
<dict>
@@ -99,7 +103,7 @@
<key>settings</key>
<dict>
<key>foreground</key>
<string>#eb6f92</string>
<string>#3e8fb0</string>
</dict>
</dict>
<dict>
@@ -112,7 +116,7 @@
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#eb6f92</string>
<string>#9ccfd8</string>
</dict>
</dict>
<dict>
@@ -123,7 +127,7 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<string></string>
<key>foreground</key>
<string>#9ccfd8</string>
</dict>
@@ -136,7 +140,7 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string> bold</string>
<string>bold</string>
<key>foreground</key>
<string>#3e8fb0</string>
</dict>
@@ -162,9 +166,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>italic</string>
<key>foreground</key>
<string>#3e8fb0</string>
<string>#ea9a97</string>
</dict>
</dict>
<dict>
@@ -175,9 +179,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<string></string>
<key>foreground</key>
<string>#f6c177</string>
<string>#c4a7e7</string>
</dict>
</dict>
<dict>
@@ -188,9 +192,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#eb6f92</string>
<string>#3e8fb0</string>
</dict>
</dict>
<dict>
@@ -203,7 +207,7 @@
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#3e8fb0</string>
<string>#c4a7e7</string>
</dict>
</dict>
<dict>
@@ -214,9 +218,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#9ccfd8</string>
<string>#ea9a97</string>
</dict>
</dict>
<dict>
@@ -227,9 +231,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#9ccfd8</string>
<string>#f6c177</string>
</dict>
</dict>
<dict>
@@ -240,7 +244,7 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<string>bold</string>
<key>foreground</key>
<string>#9ccfd8</string>
</dict>
@@ -253,7 +257,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#eb6f92</string>
</dict>
</dict>
<dict>
@@ -284,16 +290,27 @@
<string>#e0def4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation, Operators</string>
<key>scope</key>
<string>punctuation, keyword.operator</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#908caa</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>a65f621e-b84b-48fb-afec-e5c085e8debf</string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>semanticClass</key>
<string>theme.dark.rosé_pine-moon</string>
<string>theme.dark.rose-pine-moon</string>
<key>author</key>
<string>oplik0</string>
<string>arrrgi</string>
<key>comment</key>
<string>soho vibes - modified from the sublime text theme by ThatOneCalculator</string>
<string>All natural pine, faux fur and a bit of soho vibes for the classy minimalist</string>
<key>uuid</key>
<string>CC28B8FB-96BA-43EB-B71F-5AA3D3EBB0BB</string>
</dict>
</plist>

View File

@@ -30,6 +30,8 @@
<string>comment</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<key>foreground</key>
<string>#908caa</string>
</dict>
@@ -38,7 +40,7 @@
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string</string>
<string>string, punctuation.definition.string</string>
<key>settings</key>
<dict>
<key>foreground</key>
@@ -53,7 +55,7 @@
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c4a7e7</string>
<string>#f6c177</string>
</dict>
</dict>
<dict>
@@ -63,8 +65,10 @@
<string>constant.language</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
<key>foreground</key>
<string>#c4a7e7</string>
<string>#f6c177</string>
</dict>
</dict>
<dict>
@@ -75,7 +79,7 @@
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c4a7e7</string>
<string>#f6c177</string>
</dict>
</dict>
<dict>
@@ -86,9 +90,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>italic</string>
<key>foreground</key>
<string>#ebbcba</string>
<string>#e0def4</string>
</dict>
</dict>
<dict>
@@ -99,7 +103,7 @@
<key>settings</key>
<dict>
<key>foreground</key>
<string>#eb6f92</string>
<string>#31748f</string>
</dict>
</dict>
<dict>
@@ -112,7 +116,7 @@
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#eb6f92</string>
<string>#9ccfd8</string>
</dict>
</dict>
<dict>
@@ -123,7 +127,7 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<string></string>
<key>foreground</key>
<string>#9ccfd8</string>
</dict>
@@ -136,7 +140,7 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string> bold</string>
<string>bold</string>
<key>foreground</key>
<string>#31748f</string>
</dict>
@@ -162,9 +166,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>italic</string>
<key>foreground</key>
<string>#31748f</string>
<string>#ebbcba</string>
</dict>
</dict>
<dict>
@@ -175,9 +179,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<string></string>
<key>foreground</key>
<string>#f6c177</string>
<string>#c4a7e7</string>
</dict>
</dict>
<dict>
@@ -188,9 +192,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#eb6f92</string>
<string>#31748f</string>
</dict>
</dict>
<dict>
@@ -203,7 +207,7 @@
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#31748f</string>
<string>#c4a7e7</string>
</dict>
</dict>
<dict>
@@ -214,9 +218,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#9ccfd8</string>
<string>#ebbcba</string>
</dict>
</dict>
<dict>
@@ -227,9 +231,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#9ccfd8</string>
<string>#f6c177</string>
</dict>
</dict>
<dict>
@@ -240,7 +244,7 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
<string>bold</string>
<key>foreground</key>
<string>#9ccfd8</string>
</dict>
@@ -253,7 +257,9 @@
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<string>bold</string>
<key>foreground</key>
<string>#eb6f92</string>
</dict>
</dict>
<dict>
@@ -284,16 +290,27 @@
<string>#e0def4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Punctuation, Operators</string>
<key>scope</key>
<string>punctuation, keyword.operator</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#908caa</string>
</dict>
</dict>
</array>
<key>uuid</key>
<string>c3af112c-80e3-45fe-8890-43ca225fda21</string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>semanticClass</key>
<string>theme.dark.rosé_pine</string>
<string>theme.dark.rose-pine</string>
<key>author</key>
<string>oplik0</string>
<string>arrrgi</string>
<key>comment</key>
<string>soho vibes - modified from the sublime text theme by ThatOneCalculator</string>
<string>All natural pine, faux fur and a bit of soho vibes for the classy minimalist</string>
<key>uuid</key>
<string>14991673-80EB-41A2-BEFF-03216A233730</string>
</dict>
</plist>

View File

@@ -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": "<span color=\"#f53c3c\"></span> ",
"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": "<big>{:%Y %B}</big>\n<tt><small>{calendar}</small></tt>",
"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,
},
}

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,35 @@
<div align="right">
<details>
<summary >🌐 Language</summary>
<div>
<div align="center">
<a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=en">English</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=zh-CN">简体中文</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=zh-TW">繁體中文</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=ja">日本語</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=ko">한국어</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=hi">हिन्दी</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=th">ไทย</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=fr">Français</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=de">Deutsch</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=es">Español</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=it">Italiano</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=ru">Русский</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=pt">Português</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=nl">Nederlands</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=pl">Polski</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=ar">العربية</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=fa">فارسی</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=tr">Türkçe</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=vi">Tiếng Việt</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=id">Bahasa Indonesia</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=as">অসমীয়া</
</div>
</div>
</details>
</div>
# ouch.yazi
[ouch](https://github.com/ouch-org/ouch) plugin for [Yazi](https://github.com/sxyazi/yazi).

View File

@@ -2,6 +2,7 @@ local M = {}
function M:peek(job)
local child = Command("rich")
:env("COLUMNS", tostring(job.area.w))
:arg({
"-j",
"--left",

View File

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