Full Code of sdushantha/dotfiles for AI

master 69553b4a626c cached
106 files
754.3 KB
232.4k tokens
1 symbols
1 requests
Download .txt
Showing preview only (817K chars total). Download the full file or copy to clipboard to get everything.
Repository: sdushantha/dotfiles
Branch: master
Commit: 69553b4a626c
Files: 106
Total size: 754.3 KB

Directory structure:
gitextract_p0ok6nnr/

├── .gitignore
├── X11/
│   ├── .Xresources
│   └── .xinitrc
├── alacritty/
│   └── .config/
│       └── alacritty/
│           └── alacritty.toml
├── bin/
│   └── bin/
│       ├── applications/
│       │   └── radio
│       ├── bugbounty/
│       │   ├── deadlinks
│       │   └── vdp
│       ├── just4fun/
│       │   ├── 10print
│       │   ├── 2048
│       │   ├── bee
│       │   ├── groot
│       │   └── panes
│       ├── keybinded/
│       │   ├── brightness/
│       │   │   ├── brightness
│       │   │   ├── brightnessControl.sh
│       │   │   └── restoreBrightness.sh
│       │   ├── music_ctrl.sh
│       │   ├── pop_mpv.sh
│       │   ├── rofi_notes.sh
│       │   └── vifm.py
│       ├── light-theme/
│       │   └── libreoffice.sh
│       └── utils/
│           ├── 0x0
│           ├── add-shadow
│           ├── aperisolve
│           ├── border
│           ├── ce
│           ├── cnf
│           ├── darkmode.sh
│           ├── duckmail
│           ├── ew
│           ├── ex
│           ├── ffmpeg-wrappers/
│           │   ├── vid2
│           │   ├── vidcut
│           │   └── vidmute
│           ├── fwifi
│           ├── gifgen
│           ├── gym
│           ├── h2s
│           ├── kp
│           ├── mmv
│           ├── notes
│           ├── ocr
│           ├── pauseallmpv
│           ├── qrshot
│           ├── rofi-askpass
│           ├── sk
│           ├── sloc
│           ├── tmpjn
│           ├── tmpsh
│           ├── touchpad
│           ├── upld
│           ├── urldecode
│           ├── urlencode
│           ├── webcam
│           └── xcwd-helper
├── discord/
│   └── .config/
│       └── discord/
│           └── settings.json
├── dunst/
│   └── .config/
│       └── dunst/
│           └── dunstrc
├── flameshot/
│   └── .config/
│       └── flameshot/
│           └── flameshot.ini
├── gtk-2.0/
│   └── .config/
│       └── gtk-2.0/
│           └── gtkfilechooser.ini
├── gtk-3.0/
│   └── .config/
│       └── gtk-3.0/
│           ├── bookmarks
│           └── settings.ini
├── i3/
│   └── .config/
│       └── i3/
│           └── config
├── mimetype/
│   ├── .config/
│   │   └── mimeapps.list
│   └── .local/
│       └── share/
│           └── applications/
│               ├── browser.desktop
│               ├── img.desktop
│               ├── pdf.desktop
│               ├── text.desktop
│               └── video.desktop
├── mpv/
│   └── .config/
│       └── mpv/
│           ├── input.conf
│           ├── mpv.conf
│           └── scripts/
│               └── uosc.lua
├── nvim/
│   └── .config/
│       └── nvim/
│           ├── colors/
│           │   ├── idk.vim
│           │   └── test.vim
│           ├── init.lua
│           └── lua/
│               ├── mappings.lua
│               ├── options.lua
│               └── plugins/
│                   ├── configs/
│                   │   ├── bufferline.lua
│                   │   └── lualine.lua
│                   └── init.lua
├── other/
│   └── .config/
│       └── user-dirs.dirs
├── picom/
│   └── .config/
│       └── picom/
│           └── picom.conf
├── polybar/
│   └── .config/
│       └── polybar/
│           ├── config.ini
│           ├── launch.sh
│           └── scripts/
│               ├── battery_widget.sh
│               ├── bluetooth.sh
│               ├── mic_status.sh
│               ├── today.sh
│               ├── vpn-ip.sh
│               └── wifi_widget.sh
├── rofi/
│   └── .config/
│       └── rofi/
│           ├── config.rasi
│           ├── scripts/
│           │   ├── chars.txt
│           │   ├── rofi-farge.sh
│           │   ├── rofi-finder.sh
│           │   └── rofi-picker.sh
│           └── themes/
│               ├── askpass.rasi
│               ├── default.rasi
│               └── run.rasi
├── vifm/
│   └── .config/
│       └── vifm/
│           ├── colors/
│           │   ├── Default.vifm
│           │   └── minimal.vifm
│           ├── scripts/
│           │   └── README
│           ├── vifm-help.txt
│           └── vifmrc
├── wget/
│   └── .config/
│       └── wgetrc
└── zsh/
    ├── .config/
    │   └── aliases
    ├── .zprofile
    ├── .zshenv
    └── .zshrc

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
mpd/.config/mpd/mpd.log
mpd/.config/mpd/mpdstate
mpd/.config/mpd/mpd.pid
mpd/.config/mpd/mpd.db

nvim/.config/nvim/.netrwhist
nvim/.config/nvim/tmp/*
nvim/.config/nvim/plugged/*

vifminfo.json
token.txt
battery-notify.sh
packer_compiled.lua


================================================
FILE: X11/.Xresources
================================================
Xft.dpi: 120

! These might also be useful depending on your monitor and personal preference:
Xft.autohint: 0
Xft.lcdfilter:  lcddefault
Xft.hintstyle:  hintfull
Xft.hinting: 1
Xft.antialias: 1
Xft.rgba: rgb


================================================
FILE: X11/.xinitrc
================================================
#!/bin/sh
#
# ~/.xinitrc
#
# Executed by startx (run your window manager from here)
# 
# NOTICE: the exec commands MUST be the last command in this file.
#         anything after it WON'T get executed!
#

# Dont clutter the home directory
USERXSESSION="$XDG_CACHE_HOME/X11/xsession"
USERXSESSIONRC="$XDG_CACHE_HOME/X11/xsessionrc"
ALTUSERXSESSION="$XDG_CACHE_HOME/X11/Xsession"
ERRFILE="$XDG_CACHE_HOME/X11/xsession-errors"

xrdb -merge ~/.Xresources

# Set keyboard layout to Norwegian for Xorg. Seems like 'loadkeys' is not
# persistant and might only affect the TTY session. Same goes with the
# /etc/vconsole.conf
setxkbmap -layout no -variant nodeadkeys -option caps:swapescape -option altwin:swap_lalt_lwin

exec i3 --shmlog-size 0



================================================
FILE: alacritty/.config/alacritty/alacritty.toml
================================================
[font]
size = 10.4

[font.bold]
family = "JetBrainsMono NerdFont"
style = "Bold"

[font.bold_italic]
family = "JetBrainsMono NerdFont"
style = "Bold Italic"

[font.italic]
family = "JetBrainsMono NerdFont"
style = "Italic"

[font.normal]
family = "JetBrainsMono NerdFont"
style = "Regular"

[window.padding]
x = 15
y = 15


================================================
FILE: bin/bin/applications/radio
================================================
#!/usr/bin/env bash
#
# Siddharth Dushantha 2020
#

version="1.0.0"
config_file="$HOME/.config/radio/config.json"
cache_dir="$HOME/.cache/radio"
last_played="$cache_dir/last_played"
pid_file="$cache_dir/pid"
notification_icon_path="$cache_dir/icon.png"

usage(){
    cat << EOF
radio
radio -h | -l | --version
radio {pause|resume}
radio [STATION]

Play your favorite radio station from the command line with ease

Commands
kill         Same behavior as 'pause'
pause        Pause the radio.
resume       Resume the radio
stop         Same behavior as 'pause'

Options
-h, --help           Show help 
-l, --list           List available radio stations
-n, --now-playing    Show which station is playing
--version            Show version
EOF
}

print_error() {
    printf "%b\n" "Error: $1" >&2
    exit 1
}

stop(){
    kill -9 "$(cat "$pid_file")" 2> /dev/null
    # Remove the $pid_file when we stop the radio or else the script
    # will think that there is a "radio session" already playing
    rm "$pid_file" 2> /dev/null
}

play(){
    # A very hacky form of fuzzy searching.
    # We list out the station names and then use grep to find the station name
    # which the user provided. This prevents the user from having to type out
    # the exact name of the station name. The reason for using 'head -n 1' is
    # is so that we can get the first match in case there are multiple matches.
    station_name=$(printf %s "$radio_stations" | jq -r keys[] | grep -i "$1" | head -n 1)

    if [ "$station_name" = "" ]; then
        notify-send "Radio" "Could not find a radio station named \"$1\"" -i "$notification_icon_path"
        exit 1
    fi

    cover_art_url=$(printf %s "$radio_stations" | jq -r ".\"$station_name\".coverArtUrl")
    cover_art_ext=$(printf "${cover_art_url##*.}")
    cover_art_path="$cache_dir/$(printf %s "$station_name" | tr -d ' ').$cover_art_ext"
    stream_url=$(printf %s "$radio_stations" | jq -r ".\"$station_name\".streamUrl")

    # The cover art of the station is saved in teh cache so that we dont need
    # redownload it everytime the user listens to a station.
    [ ! -f "$cover_art_path" ] && curl -s "$cover_art_url" -o "$cover_art_path"

    # If the $pid_file *does not* exist, that means there is no other active radio
    # playing. But if it does exist, then we must kill the proccess otherwise
    # there would be multiple audios playing which is unpleasent.
    if [ -f "$pid_file" ]; then
        pid=$(cat "$pid_file")
        # We cannot just 'killall mpv' in order to stop any other radios "sessions"
        # from playing. This is because the user may be using mpv to view a video/image.
        # Thus, 'killall mpv' would also kill those proccesses.
        # Instead, we take the PID and get the exact command belonging to that PID. We
        # then check if the $streamUrl of the selected station is in the command
        # belonging to that PID. If so, that mean
        # shellcheck disable=SC2009
        if ! ps -p "$pid" -o args | grep "$stream_url" > /dev/null; then
            stop
        else
            # The station the user specified is already being played, so there
            # there is nothing to do.
            return 0
        fi
    fi

    # This is just for some extra ★bling★
    # Notify the user what radio is being played along with
    # the appropriate cover art.
    notify-send "Radio" "Playing $station_name" -i "$cover_art_path"

    # We save the current station name in the cache so that the user can easily
    # play/pause the radio without having to provide the station name again when
    # wanting to play the same station they previoulsy listened to.
    printf %s "$station_name" > "$last_played"

    # This is where the radio is actually played 
    mpv --no-terminal "$stream_url" &

    # Save the PID of the command above so that we can kill that proccess
    # if we need to stop the radio 
    printf %s "$!" > "$pid_file"
}

play_last_played(){
    if [ -f "$last_played" ]; then
        play "$(cat "$last_played")"
    else
        notify-send "Radio" "Couldn't find recently played station" -i "$notification_icon_path"
    fi
}

toggle(){
    if [ -f "$pid_file" ]; then
        stop
        exit
    else
        play_last_played
        exit
    fi
}

main(){
    mkdir -p "$cache_dir"

    [ ! -f "$config_file" ] && print_error "Couldn't find config file: $config_file"

    radio_stations=$(jq -r ".stations" < "$config_file")
    notification_icon_url=$(jq -r ".settings.notificationIconUrl" < "$config_file")

    [ ! -f "$notification_icon_path" ] && curl -s "$notification_icon_url" -o "$notification_icon_path"

    # Running this script wihtout any arguments, toggles the play/pause
    [ $# -eq 0 ] && toggle

    while [ "$1" ]; do
        case "$1" in
            --help | -h)
                usage
                exit ;;
            --version)
                echo "$version"
                exit ;;
            --list | -l)
                printf %s "$radio_stations" | jq -r keys[] | sed "s/^/- /g"
                exit ;;
            --now-playing | -n)
                notify-send "Radio" "Now playing $(cat $last_played)" -i "$notification_icon_path"
                exit ;;
            stop|kill|pause)
                stop
                exit ;;
            resume)
                play_last_played
                exit ;;
            -*)
                print_error "option '$1' does not exist"
                exit 1 ;;
            *)
                play "$@"
                exit ;;
        esac
        shift
    done
}

main "$@"


================================================
FILE: bin/bin/bugbounty/deadlinks
================================================
#!/usr/bin/env sh
#
# by Siddharth Dushantha 2021
#
# A wrapper around blc[1] and subfinder[2] which finds dead links that 
# may be used to find broken link hijacking vulnerabilities[3]
#
# [1] https://github.com/stevenvachon/broken-link-checker/
# [2] https://github.com/projectdiscovery/subfinder
# [3] https://gist.github.com/EdOverflow/24e0bb929169eb948bb7f3d0a2d5528f
#

version="1.0.0"
subdomain_file="/tmp/subdomains.txt"

usage(){
    cat <<EOF
deadlink -d [DOMAIN]
deadlink -u [URL]

-d, --domain DOMAIN
        Scan DOMAIN and it's subdomains for deadlinks
-u, --url URL
        Scan the provided URL for deadlinks
EOF
}

print_error() {
    printf "%b\n" "Error: $1" >&2
    exit 1
}

scan_url(){
    if ! printf "%s" "$1" | grep -Eq "https:\/\/"; then
        print_error "URL doesn't start with 'https://'"
    fi
    blc "$1" | grep "─BROKEN─"
}

scan_domain(){
    # Add the domain into the list so that it is also included
    # when we scan the subdomains for dead links.
    printf "%s\n" "$1" > "$subdomain_file"

    printf "%s\n" "Fetching all subdomains for '$1'"
    subfinder -d "$1" -silent >> "$subdomain_file"

    while read -r line; do
        printf "%s\n" "Scanning $line"
        blc "https://$line" | grep "─BROKEN─"
    done <"$subdomain_file"

    rm "$subdomain_file" 
}

main(){
    for dependency in blc subfinder; do
        if ! command -v "$dependency" >/dev/null 2>&1; then
            print_error "Could not find '$dependency', is it installed?"
        fi
    done

    [ $# -eq 0 ] && usage && exit

    while [ "$1" ]; do
        case "$1" in
            --help | -h) usage && exit ;;
            --domain | -d) scan_domain "$2" ;;
            --url| -u) scan_url "$2" ;;
            --version) echo "$version" && exit ;;
            -*) print_error "option '$1' does not exist" ;;
            *) usage && exit ;;
        esac
        shift
    done
}

main "$@"


================================================
FILE: bin/bin/bugbounty/vdp
================================================
#!/usr/bin/env bash
#
# by Siddharth Dushantha 2022
#

usage(){
    cat <<EOF
vdp [OPTIONS]

OPTIONS
-d, --domain  Domain to scan
-l, --list    Scan from a text file containing a list of domains
-t, --target  Name of target/scan. Must be used when using --list
--version     Show version
--help        Show this help message
EOF
}

for dependency in subfinder nuclei httpx; do
    if ! command -v "$dependency" >/dev/null 2>&1; then
        # Append to our list of missing dependencies
        dep_missing="$dep_missing $dependency"
    fi
done

if [ "${#dep_missing}" -gt 0 ]; then
    printf %s "Could not find the following dependencies: $dep_missing"
    exit 1
fi

while [ "$1" ]; do
    case "$1" in
        --help | -h) usage && exit ;;
        --domain | -d) domain="$2" ;;
        --list | -l) list="$2" ;;
        --target | -t) target="$2";;
        --version) echo "$version" && exit ;;
        -*) usage ;;
    esac
    shift
done


if [ ! -z "$domain" ]; then
    target=$domain
    tmp_dir="$target/tmp"
    
    mkdir -p "$target"
    mkdir -p "$tmp_dir"
    
    # If using screen or tmux, change the name of the window to the name of the target
    if ! { [ "$TERM" = "screen" ] && [ -n "$TMUX" ]; } then
              tmux rename-window -t${TMUX_PANE} "$target"
    fi

    printf %b "[\e[34mi\e[0m] Finding subdomains for $target"
    subfinder -d "$target" > "$tmp_dir/subdomains.txt" --silent
    printf %b "\e[2K\r[\e[34mi\e[0m] Found $(cat "$tmp_dir/subdomains.txt" | wc -l) subdomains on \e[34m$target\e[0m\n"

    # Remove duplicates
    sort -u "$tmp_dir/subdomains.txt" > "$tmp_dir/sorted_subdomains.txt"

    printf %b "[\e[34mi\e[0m] Removing dead subdomains\n"
    httpx -l "$tmp_dir/sorted_subdomains.txt" > "$tmp_dir/working_subdomains.txt" --silent

    printf %b "[\e[34mi\e[0m] Scanning vulnerabilities on $(cat "$tmp_dir/working_subdomains.txt" | wc -l) subdomains\n"
    nuclei -es info -list "$tmp_dir/working_subdomains.txt" -me "$target" --silent

elif [ ! -z "$list" ] && [ ! -z "$target" ]; then
    printf %b "[\e[34mi\e[0m] Scanning for vulnerabilities"
    mkdir -p "$target"

    # If using screen or tmux, change the name of the window to the name of the target
    if ! { [ "$TERM" = "screen" ] && [ -n "$TMUX" ]; } then
              tmux rename-window -t${TMUX_PANE} "$target"
    fi

    # Find vulnerabilities
    nuclei -es info -list "$list" -me "$target" --silent
fi



================================================
FILE: bin/bin/just4fun/10print
================================================
#!/usr/bin/env python3

# Creates the famous 10print art, nothing special
import random
for i in range(100000):print(chr(9585+random.randint(0,1)), end="")



================================================
FILE: bin/bin/just4fun/bee
================================================
#!/usr/bin/env python

BEE = """\n\033[1m     \033[32m"Bee" careful    \033[34m__
       \033[32mwith sudo!    \033[34m// \\
                     \\\_/ \033[33m//
   \033[35m''-.._.-''-.._.. \033[33m-(||)(')
                     '''\033[0m"""

print(BEE) 


================================================
FILE: bin/bin/just4fun/groot
================================================
       \^V//
       |. .|    I AM (G)ROOT!
     - \ - / _
      \_| |_/
        \ \
      __/_/__
     |_______|   With great power comes great responsibility.
      \     /    Use sudo wisely.
       \___/



================================================
FILE: bin/bin/just4fun/panes
================================================
#!/usr/bin/env bash

# Author: GekkoP
# Source: http://linuxbbq.org/bbs/viewtopic.php?f=4&t=1656#p33189
 
f=3 b=4
for j in f b; do
  for i in {0..7}; do
    printf -v $j$i %b "\e[${!j}${i}m"
  done
done
d=$'\e[1m'
t=$'\e[0m'
v=$'\e[7m'
 
 
cat << EOF
 
 $f1███$d▄$t  $f2███$d▄$t  $f3███$d▄$t  $f4███$d▄$t  $f5███$d▄$t  $f6███$d▄$t  $f7███$d▄$t  
 $f1███$d█$t  $f2███$d█$t  $f3███$d█$t  $f4███$d█$t  $f5███$d█$t  $f6███$d█$t  $f7███$d█$t  
 $f1███$d█$t  $f2███$d█$t  $f3███$d█$t  $f4███$d█$t  $f5███$d█$t  $f6███$d█$t  $f7███$d█$t  
 $d$f1 ▀▀▀   $f2▀▀▀   $f3▀▀▀   $f4▀▀▀   $f5▀▀▀   $f6▀▀▀   $f7▀▀▀  
EOF


================================================
FILE: bin/bin/keybinded/brightness/brightness
================================================
33.453367


================================================
FILE: bin/bin/keybinded/brightness/brightnessControl.sh
================================================
#!/usr/bin/env bash

# You can call this script like this:
# $ ./brightnessControl.sh up
# $ ./brightnessControl.sh down

# Script inspired by these wonderful people:
# https://github.com/dastorm/volume-notification-dunst/blob/master/volume.sh
# https://gist.github.com/sebastiencs/5d7227f388d93374cebdf72e783fbd6a

function get_brightness {
  xbacklight -get | cut -d '.' -f 1
}

function send_notification {
  icon="preferences-system-brightness-lock"
  brightness=$(get_brightness)
  # Make the bar with the special character ─ (it's not dash -)
  # https://en.wikipedia.org/wiki/Box-drawing_character
  bar=$(seq -s "─" 0 $((brightness / 5)) | sed 's/[0-9]//g')
  # Send the notification
  dunstify -i "$icon" -r 5555 -u low "    $bar"
}

case $1 in
  up)
    # increase the backlight by 5%
    xbacklight -inc 5

    # We output the current brightness into a file, so that when we reboot
    # or restart i3wm, the brightness can be restored by running a command
    # that is in my i3 config
    xbacklight -get > $HOME/bin/keybinded/brightness/brightness
    send_notification
    ;;
  down)
    # decrease the backlight by 5%
    xbacklight -dec 5
    xbacklight -get > $HOME/bin/keybinded/brightness/brightness
    send_notification
    ;;
esac


================================================
FILE: bin/bin/keybinded/brightness/restoreBrightness.sh
================================================
#!/usr/bin/env bash
#
# Restore the brightness by taking the value in the file, brightness

VALUE=$(cat $HOME/bin/keybinded/brightness/brightness)

xbacklight -set $VALUE


================================================
FILE: bin/bin/keybinded/music_ctrl.sh
================================================
#!/usr/bin/env bash
#
# mpDris2  is needed
_command="$1"

if [ "$1" == "toggle" ]; then
    if [ $(playerctl status) == "Paused" ]; then
        _command="play"
    else
        _command="pause"
    fi
fi



playerctl --player="spotify,mpd" "$_command"


================================================
FILE: bin/bin/keybinded/pop_mpv.sh
================================================
#!/usr/bin/env bash
#
# Created by Siddharth Dushantha (sdushantha)
#
# Dependencies: xdotool, mpv, xclip, youtube-dl
# 
# This script lets you pop almost any video from your web browser
# into mpv. If you are not using a browser, then the script will
# look in your clipboard to see if you have copied an url which
# can be played on mpv.
# 
# The reason I made this script was because I fed up of dragging
# the url into mpv everytime I wanted to view a YouTube video
# on mpv. 
# 
# ==How you can use this==
# - Copy a url to a video and run the script and it will show it in mpv
# - While on the webpage with the video, run the script, and the
#   video will be shown in mpv
#
# This script was tested using Firefox, so if you use another
# browser, replace the value for WEB_BROSWER with the name
# of your web browser (e.g Google Chrome, Opera, etc.)


# Edit this with the name of your web browser
WEB_BROWSER="Mozilla Firefox"

# Checking if the user is currently on the web browser
CURRENT=$(xdotool getwindowfocus getwindowname | grep "$WEB_BROWSER")

# Get the exit code of the command above.
# If the user is using a web browser, then the 
# exit code will be 0
STATUS=$?

# If the user is using web browser...
if [ $STATUS -eq 0 ];then
    # Then select the url bar and copy the url
    xdotool key ctrl+l
    xdotool key ctrl+c
fi

# Get the content from the clipboard
URL=$(xclip -selection clipboard -o)
    
notify-send "mpv" "Fetching video..."
mpv $URL 

# Get the exit code if mpv
STATUS=$?

if [ $STATUS -ne 0 ];then
    notify-send "mpv" "Failed to fetch the video"
    exit
fi




================================================
FILE: bin/bin/keybinded/rofi_notes.sh
================================================
#!/usr/bin/env bash
#
# Use rofi to select/create notes and then edit them using nvim
#

notes_directory="$HOME/documents/notes"
note_name=$(ls ~/documents/notes | rofi -dmenu)
note_path="$notes_directory/$note_name"

[ -n "$note_name" ] && kitty -e nvim "$note_path"


================================================
FILE: bin/bin/keybinded/vifm.py
================================================
#!/usr/bin/env python

import sys
import subprocess
import i3ipc
import os

i3 = i3ipc.Connection()

def on(i3, e):
    e.container.command('floating enable')

    e.container.command("resize set 748 px 460 px, move window to position 347 px 230 px")
    sys.exit(0)


os.popen("kitty -e /home/siddharth/bin/utils/vifmrun")

i3.on('window::new', on)
try:
    i3.main()
finally:
    i3.main_quit()


================================================
FILE: bin/bin/light-theme/libreoffice.sh
================================================
#!/usr/bin/env bash
#
# This script allows me to run libreoffice with a light GTK theme.
# To be able to get the light theme when launching the apps from your app
# launcher, edit the .desktop file for all of the libreoffice. All you have
# to do is to replace "libreoffice" with the path to this script in the exec
# variable.
# 
# Keep note, there are usually 2 "Exec" variables in each .desktop file.
#
# Example (diff):
#  - Exec=libreoffice --writer
#  + Exec=/path/to/this/script.sh --writer

GTK_THEME="Arc" libreoffice $1


================================================
FILE: bin/bin/utils/0x0
================================================
#!/usr/bin/env bash
URL="https://0x0.st"

if [ $# -eq 0 ]; then
    echo "Usage: 0x0.st FILE\n"
    exit 1
fi

FILE=$1

if [ ! -f "$FILE" ]; then
    echo "File ${FILE} not found"
    exit 1
fi

RESPONSE=$(curl -s -F "file=@${FILE}" "${URL}")

echo "${RESPONSE}"


================================================
FILE: bin/bin/utils/add-shadow
================================================
#!/usr/bin/env bash

# This script adds a cool shadow effect to images, just like MacOS screenshots.
# I usually use this for screenshots that I take with scrot
# Source: https://stefanscherer.github.io/how-to-take-screenshots-with-drop-shadow/

convert "$1" \( +clone -background grey25 -shadow 80x40+5+30 \) +swap -background transparent -layers merge +repage "$1-shadow.png"


================================================
FILE: bin/bin/utils/aperisolve
================================================
#!/usr/bin/env bash
HOST="https://www.aperisolve.com"
ARGC=$#
EXPECTED_ARGS=1

if [ $# -eq $EXPECTED_ARGS ]
then
    P=$(realpath $1) # Get File Path Browser
    REPHASH=$(curl -s -F file=@$P $HOST/upload | jq .File | tr -d '"') # Upload and get hash
    xdg-open $HOST/$REPHASH  # Open Browser
else
    echo "[?] Usage: aperisolve <file>"
fi;


================================================
FILE: bin/bin/utils/border
================================================
#!/usr/bin/env sh
#
# Siddharth Dushantha
#
# Turn the i3wm border on/off and change the size
#

set_border(){
    i3-msg "[class=.*] border pixel $1" > /dev/null 2>&1
}

# RegEx to match integers
regex="^[0-9]+$"

if [ "$1" = "on" ]; then
    set_border 1
elif [ "$1" = "off" ]; then
    set_border 0
elif printf %b "$1" | grep -Eq "$regex"; then
    set_border $1
fi



================================================
FILE: bin/bin/utils/ce
================================================
#!/usr/bin/env bash
#
# This script lets me compile and execute in one go.
# 
# Usage: ce CODE OUTPUT
#
# Example:
#  ce test.c test
#

code="$1"
output="$2"

gcc "$code" -o "$output"

./"$output"


================================================
FILE: bin/bin/utils/cnf
================================================
#!/usr/bin/env sh
#
# by Siddharth Dushantha 2021
#
# cnf - Command Not Found
#
# An utility which get the previous command that returned a
# command not found error and then checks if there is package
# which has that command. If a package is found, then it asks
# you if you want to intall it.
#
# !!README!! 
# In order for this scrip to work, create a command-not-found handler
# function in your shell's config file (e.g bashrc, zshrc, etc) and put the
# command below in the function:
#
#   mkdir -p "/tmp/command_not_found"
#   echo -n "$1" > "/tmp/command_not_found/command"
#
#   echo "zsh: command not found: $1" && exit 1
#
# Each shell has a different command-not-found handler function name:
# In the zsh its a function named command_not_found_handler[1]
# In the bash its a function named command_not_found_handle[2]
#
# [1] https://zsh.sourceforge.io/Doc/Release/Command-Execution.html#Command-Execution
# [2] https://www.gnu.org/software/bash/manual/bash.html#Command-Search-and-Execution
#

command_name=$(cat "/tmp/command_not_found/command")

# Fetch the package name which contains the file /usr/bin/COMMAND
package_name=$(pacman -Fq "/usr/bin/$command_name" | head)

# If no package is found output the error message which ZSH shows by default
if [ -z "$package_name" ]; then
    printf "%b\n" "Couldn't find the package containing the '\e[1m$command_name\e[0m' command"
    exit 1
fi

# Notify user and ask whether or not they want to install the package
printf "%b\n" "Command '\e[1m$command_name\e[0m' not found, but was found in the '\e[1m$package_name\e[0m' package."

read -p "Would you like to install it? [Y/n] " -N1 confirm

# Just adding a few blank lines so that things look clean
printf "%b" "\n\n"

if  printf %s "$confirm" | grep -Eq "[yY]"; then
    sudo pacman -S "$package_name"
fi


================================================
FILE: bin/bin/utils/darkmode.sh
================================================
#!/usr/bin/env sh


setGTKTheme(){
    # I run i3 along with GNOME services in the background, therefore
    # I'm able to use 'gsettings'. Change the command according to your system 
    if [ "$1" == "light" ]; then
        gsettings set org.gnome.desktop.interface gtk-theme "Kali-Light"
    elif [ "$1" == "dark" ]; then
        gsettings set org.gnome.desktop.interface gtk-theme "Kali-Dark"
    else
        printf %s "Error in setGTKTheme: got invalid argument '$1'"
    fi
}


setLightMode(){
    setGTKTheme light
}

setDarkMode(){
    setGTKTheme dark 
}

main(){
    while [ "$1" ]; do
        case "$1" in
            on) setDarkMode && exit ;;
            off) setLightMode && exit ;;
        esac
        shift
    done
}

main "$@"


================================================
FILE: bin/bin/utils/duckmail
================================================
#!/usr/bin/env sh
#
# by Siddharth Dushantha 2023
#
# Dependencies: jq, curl, xclip
#
# duckmail is a POSIX shell script that generates @duck.com email address using
# Duck Duck Go's Email Protection service.
#

# This file contains the auth token that is needed inorder to generate a duck email address
auth_token_path="$HOME/.config/duckmail/token.txt"
auth_token=$(cat "$auth_token_path")

# This is DuckDuckGo's logo. This image gets used as an icon for notifications thats are sent
ddg_icon_path="$HOME/.config/duckmail/ddg.png"

output(){
    # This function show output to STDOUT or as a notification depending on whether or not
    # the user executes duckmail through the terminal or a program such as 'rofi'
    message="$1"
    if [ -z "$TERM" ] || [ "$TERM" = "dumb" ]; then
        notify-send "DuckDuckGo" "$message" --icon "$ddg_icon_path"
    else
        printf "%b\n" "$message"
    fi

}

main(){
    # Iterate of the array of dependencies and check if the user has them installed.
    #
    # dep_missing allows us to keep track of how many dependencies the user is missing
    # and then print out the missing dependencies once the checking is done.
    dep_missing=""

    for dependency in jq curl xclip; do
        if ! command -v "$dependency" >/dev/null 2>&1; then
            # Append to our list of missing dependencies
            dep_missing="$dep_missing $dependency"
        fi
    done

    if [ "${#dep_missing}" -gt 0 ]; then
        printf %s "Could not find the following dependencies:$dep_missing"
        exit 1
    fi

    # The user may provide a flag such as the ones mentioed in the list below:
    #   --clipboard
    #   --copy
    #   -c
    # 
    # Since they all start with one or more '-' and a 'c' we can simply check for "-{1,2}c"
    if printf "%b" "$1" | grep -Eq -- "-{1,2}c"; then
        copy_to_clipboard=true
    fi

    # Without the auth token, we're unable to genereate a @duck.com address
    if [ ! -f "$auth_token_path" ]; then
        output "Auth token file could not be found at $auth_token_path"
        exit 1
    fi

    if [ -z "$auth_token" ];then
        output "Auth token file is empty"
        exit 1
    fi

    # Using the DuckDuckGo's Email Protection service's API endpoint, we fetch the username
    response=$(curl -s "https://quack.duckduckgo.com/api/email/addresses" -X POST -H "Authorization: Bearer $auth_token")

    if printf "%b" "$response" | grep -Eq "invalid_token"; then
        output "Your token is invalid"
        exit 1
    fi

    username=$(printf "%b" "$response" | jq -r .address)
    duck_address="$username@duck.com"

    # If $TERM is not present or is set to 'dumb', we asume the user is executing duckmail
    # through a program such as 'rofi'. Therefore, we much force duckmail to save the duck
    # adress to the clipboard as the user will be unable to copy the output sent to STDOUT
    if [ -z "$TERM" ] || [ "$TERM" = "dumb" ]; then
        copy_to_clipboard=true
    fi

    if [ "$copy_to_clipboard" = true ]; then
        printf "%b" "$duck_address" | xclip -sel c
        output "Duck address copied!"
        exit
    fi

    printf "%b\n" "$duck_address"

}

main "$@"


================================================
FILE: bin/bin/utils/ew
================================================
#!/bin/sh
#
# Siddharth Dushantha 2020
#
# https://github.com/sdushantha/bin
#
# ew - Edit Which
# Quickly edit the source code of a command. This is pretty much a short
# cut for doing --> vim $(which mycommand)

file_path=$(command -v "$1" 2>/dev/null)

if [ -z "$file_path" ]; then
    printf "%s\n" "Error: $1 not found"
    exit 1
fi

$EDITOR "$file_path"


================================================
FILE: bin/bin/utils/ex
================================================
#!/usr/bin/env bash
#
# A better way to extract archives.
# I got this from the web, so credits goes to who ever wrote this.
SAVEIFS=$IFS
IFS="$(printf '\n\t')"

extract() {
 if [ -z "$1" ]; then
    # display usage if no parameters given
    echo "Usage: extract <path/file_name>.<zip|rar|bz2|gz|tar|tbz2|tgz|Z|7z|xz|ex|tar.bz2|tar.gz|tar.xz>"
    echo "       extract <path/file_name_1.ext> [path/file_name_2.ext] [path/file_name_3.ext]"
 else
    for n in "$@"
    do
      if [ -f "$n" ] ; then
          case "${n%,}" in
            *.cbt|*.tar.bz2|*.tar.gz|*.tar.xz|*.tbz2|*.tgz|*.txz|*.tar) 
                         tar xvf "$n"       ;;
            *.lzma)      unlzma ./"$n"      ;;
            *.bz2)       bunzip2 ./"$n"     ;;
            *.cbr|*.rar)       unrar x -ad ./"$n" ;;
            *.gz)        gunzip ./"$n"      ;;
            *.cbz|*.epub)       unzip ./"$n"       ;;
            *.z)         uncompress ./"$n"  ;;
            *.7z|*.apk|*.arj|*.zip|*.cab|*.cb7|*.chm|*.deb|*.dmg|*.iso|*.lzh|*.msi|*.pkg|*.rpm|*.udf|*.wim|*.xar)
                         7z x ./"$n"        ;;
            *.xz)        unxz ./"$n"        ;;
            *.exe)       cabextract ./"$n"  ;;
            *.cpio)      cpio -id < ./"$n"  ;;
            *.cba|*.ace)      unace x ./"$n"      ;;
            *.zpaq)      zpaq x ./"$n"      ;;
            *.arc)         arc e ./"$n"       ;;
            *.cso)       ciso 0 ./"$n" ./"$n.iso" && \
                              extract "$n.iso" && \rm -f "$n" ;;
            *)
                         echo "extract: '$n' - unknown archive method"
                         return 1
                         ;;
          esac
      else
          echo "'$n' - file does not exist"
          return 1
      fi
    done
fi
}

IFS=$SAVEIFS
extract "$@"


================================================
FILE: bin/bin/utils/ffmpeg-wrappers/vid2
================================================
#!/usr/bin/env bash
#
# Convert a video to...MP4, AVI, etc
#
# usage: vid2 FILE_FORMAT FILE
#

FILE_FORMAT="$1"
FILE="$2"
OUTPUT="$FILENAME.$FILE_FORMAT"

FILENAME=$(basename -- "$2")
FILENAME="${FILENAME%.*}"

ffmpeg -hide_banner \
    -i "$FILE" \
    -codec copy \
    "$OUTPUT" 


================================================
FILE: bin/bin/utils/ffmpeg-wrappers/vidcut
================================================
#!/usr/bin/env bash
#
# Cut a video from timestamp x to y.
#
# Example:
#   vid-cut myvideo.mp4 00:01 00:12 output.mp4
#

VIDEO="$1"
FROM="$2"
TO="$3"
OUTPUT="$4"

# This is where the actual cutting happens
ffmpeg -i "$VIDEO" \
	-ss "$FROM" \
	-t "$TO" \
	-async 1 \
	"$OUTPUT"




================================================
FILE: bin/bin/utils/ffmpeg-wrappers/vidmute
================================================
#!/usr/bin/env bash
#
# Remove audio from a video file
#
# usage: vid-mute myvideo.mp4 myvideo-muted.mp4
#

INPUT="$1"
OUTPUT="$2"

ffmpeg -i "$INPUT" \
	-c copy \
	-an \
	"$OUTPUT"


================================================
FILE: bin/bin/utils/fwifi
================================================
#!/usr/bin/env bash


has() {
  local verbose=false
  if [[ $1 == '-v' ]]; then
    verbose=true
    shift
  fi
  for c in "$@"; do c="${c%% *}"
    if ! command -v "$c" &> /dev/null; then
      [[ "$verbose" == true ]] && err "$c not found"
      return 1
    fi
  done
}

err() {
  printf '\e[31m%s\e[0m\n' "$*" >&2
}

die() {
  (( $# > 0 )) && err "$*"
  exit 1
}

has -v nmcli fzf || die

SSID=$(nmcli --color yes device wifi | fzf --ansi --height=40% --reverse --cycle --inline-info --header-lines=1 | awk '{print $2}')
[[ -z "$SSID" ]] && exit
echo "connecting to \"${SSID}\"..."
nmcli -a device wifi connect "$SSID"


================================================
FILE: bin/bin/utils/gifgen
================================================
#!/usr/bin/env bash

# Echo help/usage message
show_help() {
  echo "gifgen 1.1.2"
  echo
  echo "Usage: gifgen [options] [input]"
  echo
  echo "Options:"
  echo "  -o   Output file [input.gif]"
  echo "  -f   Frames per second [10]"
  echo "  -s   Optimize for static background"
  echo "  -v   Display verbose output from ffmpeg"
  echo
  echo "Examples:"
  echo "  $ gifgen video.mp4"
  echo "  $ gifgen -o demo.gif SCM_1457.mp4"
  echo "  $ gifgen -sf 15 screencap.mov"
}

# Setup defaults
pid=$$
palette="/tmp/gif-palette-$pid.png"
fps="10"
verbosity="warning"
stats_mode="full"
dither="sierra2_4a"

# Parse args
while getopts "hi:o:f:sv" opt; do
  case "$opt" in
    h)
      show_help=true
      ;;
    o)
      output=$OPTARG
      ;;
    f)
      fps=$OPTARG
      ;;
    s)
      stats_mode="diff"
      dither="none"
      ;;
    v)
      verbosity="info"
      ;;
  esac
done
shift "$((OPTIND-1))"

# Grab input file from end of command
input=$1

# Show help and exit if we have no input
[[ "$input" = "" ]] || [[ $show_help = true ]] && show_help && exit

# Check for ffmpeg before encoding
type ffmpeg >/dev/null 2>&1 || {
  echo "Error: gifgen requires ffmpeg to be installed"
  exit 1
}

# Set output if not specified
if [[ "$output" = "" ]]; then
  input_filename=${input##*/}
  output=${input_filename%.*}.gif
fi

echo -e "[\033[1mI\033[0m] Using video \033[1m$input\033[0m"
echo -e "[\033[1mI\033[0m] Extracting frames from video"

# Encode GIF
ffmpeg -v "$verbosity" -i "$input" -vf "fps=$fps,palettegen=stats_mode=$stats_mode" -y "$palette"

[[ "$verbosity" = "info" ]] && echo

echo -e "[\033[1mI\033[0m] Encoding GIF"
ffmpeg -v "$verbosity" -i "$input" -i "$palette" -lavfi "fps=$fps [x]; [x][1:v] paletteuse=dither=$dither" -y "$output"

echo -e "[\033[1mI\033[0m] Saved GIF as \033[1m$output\033[0m"



================================================
FILE: bin/bin/utils/gym
================================================
#!/usr/bin/env python3
#
# Siddharth Dushantha 2022
#
# Check number of people at the gym 
#

import requests
import re

r = requests.get("https://spicheren.no/besokstall/")
html = r.text

total_visits = re.findall(r"Total visits: (\d+) -->", html)

print(f"Total visitors: {total_visits[0]}")


================================================
FILE: bin/bin/utils/h2s
================================================
#!/usr/bin/env sh
#
# by Siddharth Dushantha
#
# Change the HTTPS git url to a SSH git url
#

url=$(git config --get remote.origin.url)

if [ $(echo "$url" | grep "git@github.com") ]; then
    printf "%s\n" "Already SSH compatible url"
    exit
fi

username_reponame=$(echo $url | cut -d "/" -f 4-5)
ssh_url="git@github.com:$username_reponame"

git remote set-url origin "$ssh_url"
printf "Changed remote git url to SSH compatible: %s\n" "$ssh_url"


================================================
FILE: bin/bin/utils/kp
================================================
#!/usr/bin/env bash
# mnemonic: [K]ill [P]rocess
# show output of "ps -ef", use [tab] to select one or multiple entries
# press [enter] to kill selected processes and go back to the process list.
# or press [escape] to go back to the process list. Press [escape] twice to exit completely.

pid=$(ps -ef | sed 1d | eval "fzf ${FZF_DEFAULT_OPTS} -m --header='Select proccess to kill'" | awk '{print $2}')

if [ "x$pid" != "x" ]
then
  echo "$pid" | xargs kill "-${1:-9}"
  kp
fi


================================================
FILE: bin/bin/utils/mmv
================================================
#!/usr/bin/env bash
set -eu

# Lists the current directory's files in Vim, so you can edit it and save to rename them
# USAGE: vimv [file1 file2]
# https://github.com/thameera/vimv

declare -r FILENAMES_FILE=$(mktemp "${TMPDIR:-/tmp}/vimv.XXX")

trap '{ rm -f "${FILENAMES_FILE}" ; }' EXIT

if [ $# -ne 0 ]; then
    src=( "$@" )
else
    IFS=$'\r\n' GLOBIGNORE='*' command eval 'src=($(ls))'
fi

for ((i=0;i<${#src[@]};++i)); do
    echo "${src[i]}" >> "${FILENAMES_FILE}"
done

${EDITOR:-vi} "${FILENAMES_FILE}"

IFS=$'\r\n' GLOBIGNORE='*' command eval 'dest=($(cat "${FILENAMES_FILE}"))'

if (( ${#src[@]} != ${#dest[@]} )); then
    echo "WARN: Number of files changed. Did you delete a line by accident? Aborting.." >&2
    exit 1
fi

declare -i count=0
for ((i=0;i<${#src[@]};++i)); do
    if [ "${src[i]}" != "${dest[i]}" ]; then
        mkdir -p "$(dirname "${dest[i]}")"
        if git ls-files --error-unmatch "${src[i]}" > /dev/null 2>&1; then
            git mv --verbose "${src[i]}" "${dest[i]}"
        else
            mv --interactive --verbose "${src[i]}" "${dest[i]}"
        fi
        ((++count))
    fi
done

echo "$count" files renamed.


================================================
FILE: bin/bin/utils/notes
================================================
#!/usr/bin/env sh

notes_dir="$HOME/documents/notes"
file_name=$(ls "$notes_dir" | fzf)

if [ -z "$file_name" ]; then
    nvim -c "cd $notes_dir"
else
    nvim ~/documents/notes/$file_name
fi


================================================
FILE: bin/bin/utils/ocr
================================================
#!/usr/bin/env bash
#
# Siddharth Dushantha 2020
# 
# https://github.com/sdushantha/bin
#

TEXT_FILE="/tmp/ocr.txt"
IMAGE_FILE="/tmp/ocr.png"


# Check if the needed dependencies are installed
dependencies=(tesseract maim notify-send xclip)
for dependency in "${dependencies[@]}"; do
    type -p "$dependency" &>/dev/null || {
        # The reason why we are sending the error as a notification is because
        # user is most likely going to run this script by binding it to their
        # keyboard, therefor they cant see any text that is outputed using echo
        notify-send "ocr" "Could not find '${dependency}', is it installed?"
        echo "Could not find '${dependency}', is it installed?"
        exit 1
    }
done

# Take screenshot by selecting the area
maim -s "$IMAGE_FILE"

# Get the exit code of the previous command.
# So in this case, it is the screenshot command. If it did not exit with an
# exit code 0, then it means the user canceled the process of taking a
# screenshot by doing something like pressing the escape key
STATUS=$?

# If the user pressed the escape key or did something to terminate the proccess
# taking a screenshot, then just exit
[ $STATUS -ne 0 ] && exit 1

# Do the magic (∩^o^)⊃━☆゚.*・。゚
# Notice how I have removing the extension .txt from the file path. This is
# because tesseract adds .txt to the given file path anyways. So if we were to
# specify /tmp/ocr.txt as the file path, tesseract would out the text to 
# /tmp/ocr.txt.txt
tesseract "$IMAGE_FILE" "${TEXT_FILE//\.txt/}" 2> /dev/null

# Remove the new page character.
# Source: https://askubuntu.com/a/1276441/782646
sed -i 's/\x0c//' "$TEXT_FILE"

# Check if the text was detected by checking number
# of lines in the file
NUM_LINES=$(wc -l < $TEXT_FILE)
if [ "$NUM_LINES" -eq 0 ]; then
    notify-send "ocr" "no text was detected"
    exit 1
fi

# Copy text to clipboard
xclip -selection clip < "$TEXT_FILE"

# Send a notification with the text that was grabbed using OCR
notify-send "ocr" "$(cat $TEXT_FILE)"

# Clean up
# "Always leave the area better than you found it" 
#                       - My first grade teacher
rm "$TEXT_FILE"
rm "$IMAGE_FILE"


================================================
FILE: bin/bin/utils/pauseallmpv
================================================
#!/usr/bin/env bash
for i in /tmp/mpvsoc*; do
    [ -e "$i" ] || break
	echo '{ "command": ["set_property", "pause", true] }' | socat - "$i";
done


================================================
FILE: bin/bin/utils/qrshot
================================================
#!/usr/bin/env bash
#
# Siddharth Dushantha 2022
# 
# https://github.com/sdushantha/bin
#

image_file="/tmp/ocr.png"

# Check if the needed dependencies are installed
dependencies=(maim notify-send zbarimg xclip)
for dependency in "${dependencies[@]}"; do
    type -p "$dependency" &>/dev/null || {
        # The reason why we are sending the error as a notification is because
        # user is most likely going to run this script by binding it to their
        # keyboard, therefor they cant see any text that is outputed using echo
        notify-send "ocr" "Could not find '${dependency}', is it installed?"
        echo "Could not find '${dependency}', is it installed?"
        exit 1
    }
done

# Take screenshot by selecting the area
maim -s "$image_file"

# Get the exit code of the previous command.
# So in this case, it is the screenshot command. If it did not exit with an
# exit code 0, then it means the user canceled the process of taking a
# screenshot by doing something like pressing the escape key
status=$?

# If the user pressed the escape key or did something to terminate the proccess
# taking a screenshot, then just exit
[ $status -ne 0 ] && exit 1

# Use zbarimg to decode the text from the QR code
decoded_text=$(zbarimg "$image_file" -q --raw)

if [ -z "$decoded_text" ]; then
    notify-send "qrshot" "no qr code was found"
    rm $image_file && exit 1
fi

# Copy text to clipboard
printf %b "$decoded_text" | xclip -selection clip

# Let us know that something was decoded
notify-send "qrshot" "$decoded_text"

# Cleaning up the trash that was left behind
rm $image_file


================================================
FILE: bin/bin/utils/rofi-askpass
================================================
#!/usr/bin/env bash
rofi -dmenu\
    -password\
    -i\
    -no-fixed-num-lines\
    -p "Password:"\
    -theme themes/askpass.rasi


================================================
FILE: bin/bin/utils/sk
================================================
#!/usr/bin/env bash
#
# Toggle screenkey
#

if pgrep screenkey > /dev/null 2>&1; then
    killall screenkey > /dev/null 2>&1 &
    notify-send "Screenkey" "Turned off"
else
    screenkey \
        --geometry 350x700-20+430 \
        --font "JetBrains Mono Nerd Font" \
        --bg-color "#101010" \
        --font-color "#e9e4e4" \
        --no-systray \
        > /dev/null 2>&1 &
    notify-send "Screenkey" "Turned on"

fi


================================================
FILE: bin/bin/utils/sloc
================================================
#!/bin/sh
#
# http://github.com/mitchweaver/bin
#
# count lines of code in a shellscript
# ignores comments and blank lines
#

usage() {
    >&2 printf 'Usage: %s [file] or %s < [file]\n' "${0##*/}" "${0##*/}"
    exit 1
}

if [ "$1" ] ; then
    case ${1#-} in
        h)
            usage
            ;;
        *)
            [ -f "$1" ] || usage
    esac

    printf '%s' 'SLOC: '
    sed '/^\s*#/d;/^\s*$/d' "$1" | wc -l | sed 's/ //g'
else
    printf '%s' 'SLOC: '
    sed '/^\s*#/d;/^\s*$/d' | wc -l | sed 's/ //g'
fi


================================================
FILE: bin/bin/utils/tmpjn
================================================
#!/usr/bin/env sh
#
# by Siddharth Dushantha 2020
#
# tmpjn - Temporary Jupyter Notebook
#

nb_file_name="notebook.ipynb"

cd "$(mktemp -d)"

# The content of an "empty" Jupyter Notebook file.
# Even though the file is not empty, Jupyter Notebook will
# detect that this a Interactive Python Notebook.
cat >"$nb_file_name" << EOL
{
 "cells": [],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 5
}
EOL

# Open the "empty" Notebook
jupyter notebook "$nb_file_name"


================================================
FILE: bin/bin/utils/tmpsh
================================================
#!/usr/bin/env bash
#
# http://github.com/mitchweaver
#
# open a shell in a temporary dir without adding commands to history
#

cd "$(mktemp -d)" || exit
zsh


================================================
FILE: bin/bin/utils/touchpad
================================================
#!/usr/bin/env bash
#
# Siddharth Dushantha 2021
#
# Disable/enable the touchpad
#

position=$(xinput list --name-only | grep -n "Touchpad" | cut -d : -f 1)
touchpad_id=$(xinput list --id-only | sed -n "$position p")
touchpad_status=$(xinput list-props 12 | grep "Device Enabled" | cut -d : -f 2)

if [ "$touchpad_status" -eq 1 ]; then
    xinput disable "$touchpad_id"
    notify-send "Touchpad" "Disabled touchpad"

elif [ "$touchpad_status" -eq 0 ]; then
    xinput enable "$touchpad_id"
    notify-send "Touchpad" "Enabled touchpad"

else
    notify "Touchpad" "Unknown status: $touchpad_status"

fi


================================================
FILE: bin/bin/utils/upld
================================================
#!/usr/bin/env sh
if [ $# -eq 0 ];then
    echo -e "No arguments specified.\nUsage:\n  transfer <file|directory>\n  ... | transfer <file_name>">&2
    exit 1
fi

if tty -s;then
    file="$1"
    file_name=$(basename "$file")
    if [ ! -e "$file" ];then
        echo "$file: No such file or directory">&2
        return 1
    fi
    if [ -d "$file" ];then
        file_name="$file_name.zip" ,
        (cd "$file"&&zip -r -q - .)|curl --progress-bar --upload-file "-" "https://transfer.sh/$file_name"|tee /dev/null,
    else
        cat "$file"|curl --progress-bar --upload-file "-" "https://transfer.sh/$file_name"|tee /dev/null
    fi
else
    file_name=$1;curl --progress-bar --upload-file "-" "https://transfer.sh/$file_name"|tee /dev/null
fi


================================================
FILE: bin/bin/utils/urldecode
================================================
#!/usr/bin/env python3
import sys
import urllib.parse

print(urllib.parse.unquote_plus(sys.argv[1]))


================================================
FILE: bin/bin/utils/urlencode
================================================
#!/usr/bin/env python3
import sys, urllib.parse
print(urllib.parse.quote_plus(sys.argv[1]))


================================================
FILE: bin/bin/utils/webcam
================================================
#!/usr/bin/env bash
#
# Show webcam
#

mpv --demuxer-lavf-format=video4linux2 \
    --demuxer-lavf-o-set=input_format=mjpeg av://v4l2:"/dev/video0" \
    --profile=low-latency \
    --untimed \
    --vf=hflip \
    --no-keepaspect-window &> /dev/null &


================================================
FILE: bin/bin/utils/xcwd-helper
================================================
#!/usr/bin/env bash
#
# by Siddharth Dushantha 2023
#
# A script that only allows xcwd to be used for opening a terminal from certain applications.
# If the current window is Discord, the xcwd will return '/usr/bin/' and that's not where we
# want to open our terminal. So 'xcwd' only works properly when launching while our focused
# window is a terminal such as Alacritty. Thunar used work, but no longer works.
#
# Example usage:
#   alacritty --working-directory=$(xcwd-helper)
#

current_dir="$(xcwd)"
allowed_program_classes="Alacritty"
active_window_class=$(xdotool getactivewindow getwindowclassname)

if ! printf %s "$allowed_program_classes" | grep -q "$active_window_class"; then
    printf %s "$HOME"
    exit
fi

printf %s "$current_dir"



================================================
FILE: discord/.config/discord/settings.json
================================================
{
  "chromiumSwitches": {},
  "IS_MAXIMIZED": false,
  "IS_MINIMIZED": false,
  "WINDOW_BOUNDS": {
    "x": 0,
    "y": 24,
    "width": 1536,
    "height": 936
  },
  "SKIP_HOST_UPDATE": true
}

================================================
FILE: dunst/.config/dunst/dunstrc
================================================
[global]
   monitor = 0

    # If this option is set to mouse or keyboard, the monitor option
    # will be ignored.
    follow = mouse

    # Geometery reference --> [{width}]x{height}[+/-{x}+/-{y}]
    geometry = "300x0-12+37"

    # Radius of the four corners of the notification
    corner_radius = 5

    # Show how many messages are currently hidden (because of geometry).
    indicate_hidden = yes

    # Shrink window if it's smaller than the width.  Will be ignored if width is 0.
    shrink = no

    # The transparency of the window.  Range: [0; 100].
    transparency = 0
    
    # The height of the entire notification.  If the height is smaller
    # than the font height and padding combined, it will be raised
    # to the font height and padding.
    notification_height = 0

    # Show multiple notifications in the same box
    separator_height = 2

    # Define a color for the separator.
    # possible values are:
    #  * auto: dunst tries to find a color fitting to the background;
    #  * foreground: use the same color as the foreground;
    #  * frame: use the same color as the frame;
    #  * anything else will be interpreted as a X color.
    separator_color = auto

    # Add vertical padding to the inside of the notification
    padding = 10

    # Add horizontal padding for when the text gets long enough
    horizontal_padding = 10

    # The frame color and width of the notification
    frame_width = 2
    frame_color = "#333333"


    sort = yes

    # How long a user needs to be idle for sticky notifications
    idle_threshold = 120

    # Font and typography settings
    font = JetBrains Mono Nerdfont 10
    alignment = left
    word_wrap = yes

    # The spacing between lines.  If the height is smaller than the font height, it will get raised to the font height.
    line_height = 0

    # Allow some HTML tags like <i> and <u> in notifications
    markup = full

    # Format for how notifications will be displayed
    #format = "<b>%s</b>\n%b"
    format = "<span foreground='#f3f4f5'><b>%s %p</b></span>\n%b"

    show_age_threshold = 60

    # When word_wrap is set to no, specify where to make an ellipsis in long lines.
    # Possible values are "start", "middle" and "end".
    ellipsize = middle

    # Ignore newlines '\n' in notifications.
    ignore_newline = no

    # Stack together notifications with the same content
    stack_duplicates = true

    # Hide the count of stacked notifications with the same content
    hide_duplicate_count = true 

    # Display indicators for URLs (U) and actions (A).
    show_indicators = no

    # Align icons left/right/off
    icon_position = left

    # Scale larger icons down to this size, set to 0 to disable
    max_icon_size = 48

    icon_path = /usr/share/icons/Paper/16x16/status/:/usr/share/icons/Paper/16x16/devices/:/usr/share/icons/Paper/16x16/apps/

    sticky_history = yes
    history_length = 20

    # Always run rule-defined scripts, even if the notification is suppressed
    always_run_script = true

    startup_notification = false

    force_xinerama = false

    ### mouse
    # Defines action of mouse event
    # Possible values are:
    # * none: Don't do anything.
    # * do_action: If the notification has exactly one action, or one is marked as default,
    #              invoke it. If there are multiple and no default, open the context menu.
    # * close_current: Close current notification.
    # * close_all: Close all notifications.
    mouse_left_click = do_action
    mouse_middle_click = close_all
    mouse_right_click = close_current

[urgency_low]
    # This urgency should be used only 
    # for volume/brightness notification
    background = "#111116"
    foreground = "#a8a8a8"
    timeout = 1
    icon = /dev/null

[urgency_normal]
    background = "#111116"
    foreground = "#a8a8a8"
    timeout = 10
    icon = /dev/null

[urgency_critical]
    background = "#d64e4e"
    foreground = "#f0e0e0"
    frame_color = "#d64e4e"
    timeout = 0
    icon = /usr/share/icons/Paper/16x16/status/dialog-warning.png

[skip-rule] 
appname=discord
skip_display=true



================================================
FILE: flameshot/.config/flameshot/flameshot.ini
================================================
[General]
checkForUpdates=false
contrastOpacity=102
contrastUiColor=#7c8fa3
disabledTrayIcon=true
drawColor=#ff0000
drawFontSize=27
drawThickness=4
filenamePattern=%F-%H%M%S
saveAfterCopy=true
saveAsFileExtension=png
savePath=/home/siddharth/pictures/screenshots
savePathFixed=true
showDesktopNotification=false
showHelp=false
showSidePanelButton=false
showStartupLaunchMessage=false
startupLaunch=false
uiColor=#abc4e0
undoLimit=104
uploadHistoryMax=22

[Shortcuts]
TYPE_COPY=Return


================================================
FILE: gtk-2.0/.config/gtk-2.0/gtkfilechooser.ini
================================================
[Filechooser Settings]
LocationMode=path-bar
ShowHidden=false
ShowSizeColumn=true
GeometryX=1020
GeometryY=0
GeometryWidth=840
GeometryHeight=630
SortColumn=name
SortOrder=ascending
StartupMode=recent


================================================
FILE: gtk-3.0/.config/gtk-3.0/bookmarks
================================================
file:///home/siddharth/documents
file:///home/siddharth/downloads
file:///home/siddharth/pictures
file:///home/siddharth/videos
file:///home/siddharth/pictures/screenshots
file:///home/siddharth/projects


================================================
FILE: gtk-3.0/.config/gtk-3.0/settings.ini
================================================
[Settings]
gtk-theme-name=Arc-Dark
gtk-icon-theme-name=Paper
gtk-font-name=Noto Sans 11
gtk-cursor-theme-name=Adwaita
gtk-cursor-theme-size=0
gtk-toolbar-style=GTK_TOOLBAR_BOTH
gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
gtk-button-images=1
gtk-menu-images=1
gtk-enable-event-sounds=1
gtk-enable-input-feedback-sounds=1
gtk-xft-antialias=1
gtk-xft-hinting=1
gtk-xft-hintstyle=hintfull
gtk-xft-rgba=rgb


================================================
FILE: i3/.config/i3/config
================================================
# Norwegian speacial letters
# Æ = ae
# Ø = oslash
# Å = aring

# General {{{  
# Define names for default workspaces for which we configure key bindings later on.
# We use variables to avoid repeating the names in multiple places.
set $ws1 "1"
set $ws2 "2"
set $ws3 "3"
set $ws4 "4"
set $ws5 "5"
set $ws6 "6"
set $ws7 "7"
set $ws8 "8"
set $ws9 "9"
set $ws10 "10"

# The pixles of the gaps
gaps inner 0
gaps outer 0
smart_borders on

font pango:Hack 9
#}}}


# Keybindings {{{ 

set $mod Mod4

# Use Mouse+$mod to drag floating windows to their wanted position
floating_modifier $mod

# kill focused window
bindsym $mod+q kill

# change focus
bindsym $mod+h focus left
bindsym $mod+j focus down
bindsym $mod+k focus up
bindsym $mod+l focus right

# move focused window
bindsym $mod+Shift+h move left
bindsym $mod+Shift+j move down
bindsym $mod+Shift+k move up
bindsym $mod+Shift+l move right

# split in horizontal orientation
bindsym $mod+period split h

# split in vertical orientation
bindsym $mod+comma split v

# enter fullscreen mode for the focused container
bindsym $mod+f fullscreen toggle

# change container layout (tacked, tabbed, toggle split)
bindsym $mod+t layout tabbed 
bindsym $mod+Shift+t layout splith

# toggle tiling / floating
bindsym $mod+Shift+space floating toggle,move absolute position center

# focus the parent container
bindsym $mod+a focus parent

# switch to workspace
bindsym $mod+1 workspace $ws1
bindsym $mod+2 workspace $ws2
bindsym $mod+3 workspace $ws3
bindsym $mod+4 workspace $ws4
bindsym $mod+5 workspace $ws5
bindsym $mod+6 workspace $ws6
bindsym $mod+7 workspace $ws7
bindsym $mod+8 workspace $ws8
bindsym $mod+9 workspace $ws9
bindsym $mod+0 workspace $ws10

# move focused container to workspace
bindsym $mod+Shift+1 move container to workspace $ws1
bindsym $mod+Shift+2 move container to workspace $ws2
bindsym $mod+Shift+3 move container to workspace $ws3
bindsym $mod+Shift+4 move container to workspace $ws4
bindsym $mod+Shift+5 move container to workspace $ws5
bindsym $mod+Shift+6 move container to workspace $ws6
bindsym $mod+Shift+7 move container to workspace $ws7
bindsym $mod+Shift+8 move container to workspace $ws8
bindsym $mod+Shift+9 move container to workspace $ws9
bindsym $mod+Shift+0 move container to workspace $ws10

# Restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
bindsym $mod+Shift+r restart

# Exit i3 (logs you out of your X session)
bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'You pressed the exit shortcut. Do you really want to exit i3? This will end your X session.' -b 'Yes, exit i3' 'i3-msg exit'"

# Resize window
# Mod1 = Alt
bindsym $mod+Mod1+h resize shrink width 1 px or 1 ppt
bindsym $mod+Mod1+j resize grow height 1 px or 1 ppt
bindsym $mod+Mod1+k resize shrink height 1 px or 1 ppt
bindsym $mod+Mod1+l resize grow width 1 px or 1 ppt

# Launch terminal
bindsym $mod+Return exec alacritty --working-directory="$(command -v xcwd >/dev/null && xcwd | awk -F '\n' '{print $1}'|| echo $HOME)" 

# Launch a terminal with vifm
bindsym $mod+Shift+Return exec alacritty --working-directory="$(command -v xcwd >/dev/null && xcwd | awk -F '\n' '{print $1}' || echo $HOME)" -e vifmrun

# Keybindings that do things using rofi
bindsym $mod+space exec --no-startup-id rofi -show drun
bindsym $mod+n exec bash $HOME/bin/keybinded/rofi_notes.sh 
bindsym $mod+ae exec bash $HOME/bin/keybinded/rofi_todo.sh
bindsym $mod+v exec rofi -modi "clipboard:greenclip print" -show clipboard -run-command '{cmd}' && xdotool key ctrl+shift+v
bindsym $mod+Shift+d exec bash $HOME/.config/rofi/scripts/rofi-picker.sh
bindsym $mod+o exec zsh -c "bash $HOME/.config/rofi/scripts/rofi-finder.sh $HOME"
bindsym $mod+Shift+c exec bash $HOME/.config/rofi/scripts/rofi-farge.sh

# Lauch my Calculator 
bindsym XF86Calculator exec qalculate-gtk 

# Volume controls
bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ 0 && pactl set-sink-volume @DEFAULT_SINK@ +5% 
bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ 0 && pactl set-sink-volume @DEFAULT_SINK@ -5% 
bindsym XF86AudioMute exec --no-startup-id pactl set-sink-mute @DEFAULT_SINK@ toggle

# Music control
bindsym XF86AudioNext exec --no-startup-id bash $HOME/bin/keybinded/music_ctrl.sh next  
bindsym XF86AudioPrev exec --no-startup-id bash $HOME/bin/keybinded/music_ctrl.sh previous
bindsym XF86AudioPlay exec --no-startup-id bash $HOME/bin/keybinded/music_ctrl.sh toggle
bindsym XF86AudioStop exec --no-startup-id bash $HOME/bin/keybinded/music_ctrl.sh pause 

# Toggle mic
bindsym XF86AudioMicMute exec --no-startup-id pactl set-source-mute 0 toggle

# Brightness control
bindsym XF86MonBrightnessUp exec brightnessctl s 5%+
bindsym XF86MonBrightnessDown exec brightnessctl s 5%-

# Screenshot selected area
# You need to use `--release` in the binding.
# See "4.3. Keyboard bindings" on i3wm docs 
bindsym $mod+Shift+x --release exec flameshot gui

# Select color from screen and save the value to clipboard 
bindsym $mod+c --release exec farge --no-color-code --no-preview

# Rotating the display. Keybinings taken from Windows.
bindsym Control+Mod1+Up exec xrandr -o normal
bindsym Control+Mod1+Down exec xrandr -o inverted 
bindsym Control+Mod1+Right exec xrandr -x 

# Hide/unhide windows. A little similar to minimizing/maximizing
# windows on a DE 
bindsym $mod+m move scratchpad
bindsym $mod+shift+m scratchpad show

# Control dunst notifications
bindsym Control+space exec dunstctl close
#bindsym Control+Shift+space exec dunstctl all 
bindsym Control+Shift+period exec dunstctl context 

# Lock my screen
bindsym $mod+x exec betterlockscreen --lock

# Launch Thunar
bindsym $mod+E exec thunar &

#}}}


# Autorun {{{ 
# exec -> On start-up
# exec_always -> On start-up and reload
exec_always --no-startup-id feh pictures/current/* --bg-fill --no-fehbg
exec_always --no-startup-id picom
exec_always --no-startup-id bash $HOME/bin/keybinded/brightness/restoreBrightness.sh
exec_always --no-startup-id bash $HOME/.config/polybar/launch.sh
exec_always --no-startup-id dunst
exec_always --no-startup-id i3-auto-layout 
exec_always --no-startup-id flameshot 
exec_always --no-startup-id xfce4-power-manager
exec_always --no-startup-id greenclip daemon
exec_always --no-startup-id nm-applet 

#exec --no-startup-id /usr/lib/gsd-xsettings
exec --no-startup-id /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
#}}}


# Window rules {{{ 
# Gives a border to the windows. 
for_window [class="^.*"] border pixel 1

# class                 border  backgr. text    indicator child_border
client.focused          #2c2f3e #2c2f3e #F5C2E7 #8c8c8c   #8c8c8c
client.focused_inactive #2c2f3e #2c2f3e #F5C2E7 #333333   #333333
client.unfocused        #121317 #121317 #D9E0EE #333333   #333333
client.urgent           #f28fad #f28fad #000000 #333333   #f28fad
client.placeholder      #333333 #333333 #000000 #333333   #333333

# Dialogs, popups, etc should be floating and in the center of the screen
for_window [window_role="task_dialog"]      floating enable, move absolute position center, border pixel 0
for_window [window_role="Dialog"]           floating enable, move absolute position center, border pixel 0
for_window [window_role="pop-up"]           floating enable, move absolute position center                     
for_window [window_role="bubble"]           floating enable, move absolute position center
for_window [window_role="Preferences"]      floating enable, move absolute position center
for_window [window_type="dialog"]           floating enable, move absolute position center, border pixel 0
for_window [window_type="menu"]             floating enable, move absolute position center
for_window [title="(Open File|File Upload)"] floating enable, move absolute position center

for_window [class="zoom"] floating enable
for_window [class="[Bb]lueberry.py"] floating enable
for_window [class="mpv"] floating enable
for_window [class="[qQ]alculate-gtk"] floating enable
for_window [class="[Ss]imple[Ss]creen[Rr]ecorder"] floating enable
for_window [class="[Gg]nome-calendar"] floating enable
for_window [class="[Dd]ragon-drag-and-drop"] floating enable, border pixel 0
for_window [class="Windscribe[2]"] floating enable, border pixel 0
for_window [class="die"] floating enable
for_window [class="[Xx][Cc]alc"] floating enable
for_window [class="[N]sxiv"] floating enable

# Prevent mouse from changing the focus
focus_follows_mouse yes

# Fixes graphics glitch
new_window none
#}}}



================================================
FILE: mimetype/.config/mimeapps.list
================================================
[Default Applications]
inode/directory=thunar.desktop;
text/x-shellscript=text.desktop;
text/plain=text.desktop;
text/x-makefile=text.desktop;
image/png=img.desktop;
image/jpeg=img.desktop;
image/gif=img.desktop;
application/pdf=pdf.desktop;
video/x-matroska=video.desktop;
video/mp4=video.desktop;
x-scheme-handler/http=browser.desktop;
x-scheme-handler/https=browser.desktop;

[Added Associations]
application/json=text.desktop;


================================================
FILE: mimetype/.local/share/applications/browser.desktop
================================================
[Desktop Entry]
Type=Application
Name=Web Browser
Exec=/usr/bin/firefox %u


================================================
FILE: mimetype/.local/share/applications/img.desktop
================================================
[Desktop Entry]
Type=Application
Name=Image viewer
Exec=/usr/bin/nsxiv -a %f


================================================
FILE: mimetype/.local/share/applications/pdf.desktop
================================================
[Desktop Entry]
Type=Application
Name=PDF reader
Exec=/usr/bin/firefox  %u


================================================
FILE: mimetype/.local/share/applications/text.desktop
================================================
[Desktop Entry]
Type=Application
Name=Text editor
Exec=/usr/bin/alacritty -e nvim %u


================================================
FILE: mimetype/.local/share/applications/video.desktop
================================================
[Desktop Entry]
Type=Application
Name=Video viewer
Exec=/usr/bin/mpv -quiet "%u"


================================================
FILE: mpv/.config/mpv/input.conf
================================================
# Seeking
l seek 5    # Forward
h seek -5   # Rewind

# Volume controle
j add volume -2   # Decrease volume
k add volume 2    # Increase volume

# Hit the space bar to play/pause
SPACE cycle pause

# Quit mpv
q quit

# Zoom in and out
+ add video-zoom 0.1
- add video-zoom -0.1

# Vim like keybindings to pan
H add video-pan-x  0.1    # Pan left
L add video-pan-x -0.1    # Pan right
K add video-pan-y  0.1    # Pan up
J add video-pan-y -0.1    # Pan down

# Reset the panning and zooming
= set video-zoom 0 ; set video-pan-x 0 ; set video-pan-y 0

# Mute/Unmute
m cycle mute

# Next/Prev in playlist
n playlist-next
p playlist-prev

# Disable the arrow keys because I rather
# get used to VIM keys
left ignore
right ignore
up ignore
down ignore

# Rotate
Ctrl+r no-osd cycle-values video-rotate  "90" "180" "270" "0"

# Open curren file in dragon so you can drag and drop it
Ctrl+o run "/bin/bash" "-c" "dragon-drag-and-drop \"${path}\""

# Copy the full path of the current file
Ctrl+c run "/bin/bash" "-c" "echo $PWD/${path} | xclip -selection c && dunstify mpv \"File path copied to clipboard\""

# Copy only the name of the file
Ctrl+Shift+c run "/bin/bash" "-c" "echo ${path} | xclip -selection c && dunstify mpv \"File name copied to clipboard\""


================================================
FILE: mpv/.config/mpv/mpv.conf
================================================
# Adjusting the initial window size
geometry=36%

# Disable On Screen Controlers
osc=no

# uosc provides its own seeking/volume indicators, so you also don't need this
osd-bar=no
# uosc will draw its own window controls if you disable window border
border=no

# Enable the best hardware decoder
hwdec=yes

# If the current file is an image, keep
# it open forever
image-display-duration=inf

# Loops the playlist forever
loop-playlist=inf

# Loop files in case of webms or gifs
loop-file=inf

# I honestly dont know what these lines
# do, but all I know is that these lines
# allow me to display images properly
# source: https://git.io/fjvtn
scale=spline36
cscale=spline36
dscale=mitchell
dither-depth=auto
correct-downscaling
sigmoid-upscaling

script-opts=ytdl_hook-ytdl_path=yt-dlp

[extension.mp3]
geometry=250x250



================================================
FILE: mpv/.config/mpv/scripts/uosc.lua
================================================
--[[

uosc 2.16.0 - 2022-Mar-21 | https://github.com/darsain/uosc

Minimalist cursor proximity based UI for MPV player.

uosc replaces the default osc UI, so that has to be disabled first.
Place these options into your `mpv.conf` file:

```
# required so that the 2 UIs don't fight each other
osc=no
# uosc provides its own seeking/volume indicators, so you also don't need this
osd-bar=no
# uosc will draw its own window controls if you disable window border
border=no
```

Options go in `script-opts/uosc.conf`. Defaults:

```
# timeline size when fully retracted, 0 will hide it completely
timeline_size_min=2
# timeline size when fully expanded, in pixels, 0 to disable
timeline_size_max=40
# same as ^ but when in fullscreen
timeline_size_min_fullscreen=0
timeline_size_max_fullscreen=60
# same thing as calling toggle-progress command once on startup
timeline_start_hidden=no
# comma separated states when timeline should always be visible. available: paused, audio
timeline_persistency=
# timeline opacity
timeline_opacity=0.8
# top border of background color to help visually separate timeline from video
timeline_border=1
# when scrolling above timeline, wheel will seek by this amount of seconds
timeline_step=5
# display seekable buffered ranges for streaming videos, syntax `color:opacity`,
# color is an BBGGRR hex code, set to `none` to disable
timeline_cached_ranges=345433:0.5
# floating number font scale adjustment
timeline_font_scale=1

# timeline chapters style: none, dots, lines, lines-top, lines-bottom
chapters=dots
chapters_opacity=0.3

# where to display volume controls: none, left, right
volume=right
volume_size=40
volume_size_fullscreen=60
volume_persistency=
volume_opacity=0.8
volume_border=1
volume_step=1
volume_font_scale=1

# playback speed widget: mouse drag or wheel to change, click to reset
speed=no
speed_size=46
speed_size_fullscreen=68
speed_persistency=
speed_opacity=1
speed_step=0.1
speed_font_scale=1

# controls all menus, such as context menu, subtitle loader/selector, etc
menu_item_height=36
menu_item_height_fullscreen=50
menu_wasd_navigation=no
menu_hjkl_navigation=no
menu_opacity=0.8
menu_font_scale=1

# menu button widget
# can be: never, bottom-bar, center
menu_button=never
menu_button_size=26
menu_button_size_fullscreen=30
menu_button_persistency=
menu_button_opacity=1
menu_button_border=1

# top bar with window controls and media title
# can be: never, no-border, always
top_bar=no-border
top_bar_size=40
top_bar_size_fullscreen=46
top_bar_persistency=
top_bar_controls=yes
top_bar_title=yes

# window border drawn in no-border mode
window_border_size=1
window_border_opacity=0.8

# pause video on clicks shorter than this number of milliseconds, 0 to disable
pause_on_click_shorter_than=0
# flash duration in milliseconds used by `flash-{element}` commands
flash_duration=1000
# distances in pixels below which elements are fully faded in/out
proximity_in=40
proximity_out=120
# BBGGRR - BLUE GREEN RED hex color codes
color_foreground=ffffff
color_foreground_text=000000
color_background=000000
color_background_text=ffffff
# use bold font weight throughout the whole UI
font_bold=no
# show total time instead of time remaining
total_time=no
# hide UI when mpv autohides the cursor
autohide=no
# can be: none, flash, static, manual (controlled by flash-pause-indicator and decide-pause-indicator commands)
pause_indicator=flash
# screen dim when stuff like menu is open, 0 to disable
curtain_opacity=0.5
# sizes to list in stream quality menu
stream_quality_options=4320,2160,1440,1080,720,480,360,240,144
# load first file when calling next on a last file in a directory and vice versa
directory_navigation_loops=no
# file types to look for when navigating media files
media_types=3gp,avi,bmp,flac,flv,gif,h264,h265,jpeg,jpg,m4a,m4v,mid,midi,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rmvb,svg,tif,tiff,wav,weba,webm,webp,wma,wmv
# file types to look for when loading external subtitles
subtitle_types=aqt,gsub,jss,sub,ttxt,pjs,psb,rt,smi,slt,ssf,srt,ssa,ass,usf,idx,vt
# used to approximate text width
# if you are using some wide font and see a lot of right side clipping in menus,
# try bumping this up
font_height_to_letter_width_ratio=0.5

# `chapter_ranges` lets you transform chapter indicators into range indicators.
#
# Chapter range definition syntax:
# ```
# start_pattern<color:opacity>end_pattern
# ```
#
# Multiple start and end patterns can be defined by separating them with `|`:
# ```
# p1|pN<color:opacity>p1|pN
# ```
#
# Multiple chapter ranges can be defined by separating them with comma:
#
# chapter_ranges=range1,rangeN
#
# One of `start_pattern`s can be a custom keyword `{bof}` that will match
# beginning of file when it makes sense.
#
# One of `end_pattern`s can be a custom keyword `{eof}` that will match end of
# file when it makes sense.
#
# Patterns are lua patterns (http://lua-users.org/wiki/PatternsTutorial).
# They only need to occur in a title, not match it completely.
# Matching is case insensitive.
#
# `color` is a `bbggrr` hexadecimal color code.
# `opacity` is a float number from 0 to 1.
#
# Examples:
#
# Display anime openings and endings as ranges:
# ```
# chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}
# ```
#
# Display skippable youtube video sponsor blocks from https://github.com/po5/mpv_sponsorblock
# ```
# chapter_ranges=sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end
# ```
chapter_ranges=^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end
```

Available keybindings (place into `input.conf`):

```
Key  script-binding uosc/peek-timeline
Key  script-binding uosc/toggle-progress
Key  script-binding uosc/flash-timeline
Key  script-binding uosc/flash-top-bar
Key  script-binding uosc/flash-volume
Key  script-binding uosc/flash-speed
Key  script-binding uosc/flash-pause-indicator
Key  script-binding uosc/decide-pause-indicator
Key  script-binding uosc/menu
Key  script-binding uosc/load-subtitles
Key  script-binding uosc/subtitles
Key  script-binding uosc/audio
Key  script-binding uosc/video
Key  script-binding uosc/playlist
Key  script-binding uosc/chapters
Key  script-binding uosc/stream-quality
Key  script-binding uosc/open-file
Key  script-binding uosc/next
Key  script-binding uosc/prev
Key  script-binding uosc/first
Key  script-binding uosc/last
Key  script-binding uosc/next-file
Key  script-binding uosc/prev-file
Key  script-binding uosc/first-file
Key  script-binding uosc/last-file
Key  script-binding uosc/delete-file-next
Key  script-binding uosc/delete-file-quit
Key  script-binding uosc/show-in-directory
Key  script-binding uosc/open-config-directory
```
]]

if mp.get_property('osc') == 'yes' then
	mp.msg.info('Disabled because original osc is enabled!')
	return
end

local assdraw = require('mp.assdraw')
local opt = require('mp.options')
local utils = require('mp.utils')
local msg = require('mp.msg')
local osd = mp.create_osd_overlay('ass-events')
local infinity = 1e309

-- OPTIONS/CONFIG/STATE
local options = {
	timeline_size_min = 2,
	timeline_size_max = 40,
	timeline_size_min_fullscreen = 0,
	timeline_size_max_fullscreen = 60,
	timeline_start_hidden = false,
	timeline_persistency = '',
	timeline_opacity = 0.8,
	timeline_border = 1,
	timeline_step = 5,
	timeline_cached_ranges = '345433:0.5',
	timeline_font_scale = 1,

	chapters = 'dots',
	chapters_opacity = 0.3,

	volume = 'right',
	volume_size = 40,
	volume_size_fullscreen = 60,
	volume_persistency = '',
	volume_opacity = 0.8,
	volume_border = 1,
	volume_step = 1,
	volume_font_scale = 1,

	speed = false,
	speed_size = 46,
	speed_size_fullscreen = 60,
	speed_persistency = '',
	speed_opacity = 1,
	speed_step = 0.1,
	speed_font_scale = 1,

	menu_item_height = 36,
	menu_item_height_fullscreen = 50,
	menu_wasd_navigation = false,
	menu_hjkl_navigation = false,
	menu_opacity = 0.8,
	menu_font_scale = 1,

	menu_button = 'never',
	menu_button_size = 26,
	menu_button_size_fullscreen = 30,
	menu_button_opacity = 1,
	menu_button_persistency = '',
	menu_button_border = 1,

	top_bar = 'no-border',
	top_bar_size = 40,
	top_bar_size_fullscreen = 46,
	top_bar_persistency = '',
	top_bar_controls = true,
	top_bar_title = true,

	window_border_size = 1,
	window_border_opacity = 0.8,
	pause_on_click_shorter_than = 0,
	flash_duration = 1000,
	proximity_in = 40,
	proximity_out = 120,
	color_foreground = 'ffffff',
	color_foreground_text = '000000',
	color_background = '000000',
	color_background_text = 'ffffff',
	total_time = false,
	font_bold = false,
	autohide = false,
	pause_indicator = 'flash',
	curtain_opacity = 0.5,
	stream_quality_options = '4320,2160,1440,1080,720,480,360,240,144',
	directory_navigation_loops = false,
	media_types = '3gp,asf,avi,bmp,flac,flv,gif,h264,h265,jpeg,jpg,m4a,m4v,mid,midi,mkv,mov,mp3,mp4,mp4a,mp4v,mpeg,mpg,oga,ogg,ogm,ogv,opus,png,rmvb,svg,tif,tiff,wav,weba,webm,webp,wma,wmv',
	subtitle_types = 'aqt,gsub,jss,sub,ttxt,pjs,psb,rt,smi,slt,ssf,srt,ssa,ass,usf,idx,vt',
	font_height_to_letter_width_ratio = 0.5,
	chapter_ranges = '^op| op$|opening<968638:0.5>.*, ^ed| ed$|^end|ending$<968638:0.5>.*|{eof}, sponsor start<3535a5:.5>sponsor end, segment start<3535a5:0.5>segment end',
}
opt.read_options(options, 'uosc')
local config = {
	render_delay = 0.03, -- sets max rendering frequency
	font = mp.get_property('options/osd-font'),
	menu_parent_opacity = 0.4,
	menu_min_width = 260
}
local bold_tag = options.font_bold and '\\b1' or ''
local display = {
	width = 1280,
	height = 720,
	aspect = 1.77778,
}
local cursor = {
	hidden = true, -- true when autohidden or outside of the player window
	x = 0,
	y = 0,
}
local state = {
	os = (function()
		if os.getenv('windir') ~= nil then return 'windows' end
		local homedir = os.getenv('HOME')
		if homedir ~= nil and string.sub(homedir,1,6) == '/Users' then return 'macos' end
		return 'linux'
	end)(),
	cwd = mp.get_property('working-directory'),
	media_title = '',
	duration = nil,
	position = nil,
	pause = false,
	chapters = nil,
	chapter_ranges = nil,
	border = mp.get_property_native('border'),
	fullscreen = mp.get_property_native('fullscreen'),
	maximized = mp.get_property_native('window-maximized'),
	fullormaxed = mp.get_property_native('fullscreen') or mp.get_property_native('window-maximized'),
	render_timer = nil,
	render_last_time = 0,
	volume = nil,
	volume_max = nil,
	mute = nil,
	is_audio = nil, -- true if file is audio only (mp3, etc)
	cursor_autohide_timer = mp.add_timeout(mp.get_property_native('cursor-autohide') / 1000, function()
		if not options.autohide then return end
		handle_mouse_leave()
	end),
	mouse_bindings_enabled = false,
	cached_ranges = nil,
}
local forced_key_bindings -- defined at the bottom next to events

-- HELPERS

function round(number)
	local modulus = number % 1
	return modulus < 0.5 and math.floor(number) or math.ceil(number)
end

function call_me_maybe(fn, value1, value2, value3)
	if fn then fn(value1, value2, value3) end
end

function split(str, pattern)
	local list = {}
	local full_pattern = '(.-)' .. pattern
	local last_end = 1
	local start_index, end_index, capture = str:find(full_pattern, 1)
	while start_index do
		list[#list +1] = capture
		last_end = end_index + 1
		start_index, end_index, capture = str:find(full_pattern, last_end)
	end
	if last_end <= (#str + 1) then
		capture = str:sub(last_end)
		list[#list +1] = capture
	end
	return list
end

function itable_find(haystack, needle)
	local is_needle = type(needle) == 'function' and needle or function(index, value)
		return value == needle
	end
	for index, value in ipairs(haystack) do
		if is_needle(index, value) then return index, value end
	end
end

function itable_filter(haystack, needle)
	local is_needle = type(needle) == 'function' and needle or function(index, value)
		return value == needle
	end
	local filtered = {}
	for index, value in ipairs(haystack) do
		if is_needle(index, value) then filtered[#filtered + 1] = value end
	end
	return filtered
end

function itable_remove(haystack, needle)
	local should_remove = type(needle) == 'function' and needle or function(value)
		return value == needle
	end
	local new_table = {}
	for _, value in ipairs(haystack) do
		if not should_remove(value) then
			new_table[#new_table + 1] = value
		end
	end
	return new_table
end

function itable_slice(haystack, start_pos, end_pos)
	start_pos = start_pos and start_pos or 1
	end_pos = end_pos and end_pos or #haystack

	if end_pos < 0 then end_pos = #haystack + end_pos + 1 end
	if start_pos < 0 then start_pos = #haystack + start_pos + 1 end

	local new_table = {}
	for index, value in ipairs(haystack) do
		if index >= start_pos and index <= end_pos then
			new_table[#new_table + 1] = value
		end
	end
	return new_table
end

function table_copy(table)
	local new_table = {}
	for key, value in pairs(table) do new_table[key] = value end
	return new_table
end

-- Sorting comparator close to (but not exactly) how file explorers sort files
local word_order_comparator = (function()
	local symbol_order
	local default_order

	if state.os == 'win' then
		symbol_order = {
			['!'] = 1, ['#'] = 2, ['$'] = 3, ['%'] = 4, ['&'] = 5, ['('] = 6, [')'] = 6, [','] = 7,
			['.'] = 8, ["'"] = 9, ['-'] = 10, [';'] = 11, ['@'] = 12, ['['] = 13, [']'] = 13, ['^'] = 14,
			['_'] = 15, ['`'] = 16, ['{'] = 17, ['}'] = 17, ['~'] = 18, ['+'] = 19, ['='] = 20,
		}
		default_order = 21
	else
		symbol_order = {
			['`'] = 1, ['^'] = 2, ['~'] = 3, ['='] = 4, ['_'] = 5, ['-'] = 6, [','] = 7, [';'] = 8,
			['!'] = 9, ["'"] = 10, ['('] = 11, [')'] = 11, ['['] = 12, [']'] = 12, ['{'] = 13, ['}'] = 14,
			['@'] = 15, ['$'] = 16, ['*'] = 17, ['&'] = 18, ['%'] = 19, ['+'] = 20, ['.'] = 22, ['#'] = 23,
		}
		default_order = 21
	end

	return function (a, b)
		a = a:lower()
		b = b:lower()
		for i = 1, math.max(#a, #b) do
			local ai = a:sub(i, i)
			local bi = b:sub(i, i)
			if ai == nil and bi then return true end
			if bi == nil and ai then return false end
			local a_order = symbol_order[ai] or default_order
			local b_order = symbol_order[bi] or default_order
			if a_order == b_order then
				return a < b
			else
				return a_order < b_order
			end
		end
	end
end)()

-- Creates in-between frames to animate value from `from` to `to` numbers.
-- Returns function that terminates animation.
-- `to` can be a function that returns target value, useful for movable targets.
-- `speed` is an optional float between 1-instant and 0-infinite duration
-- `callback` is called either on animation end, or when animation is canceled
function tween(from, to, setter, speed, callback)
	if type(speed) ~= 'number' then
		callback = speed
		speed = 0.3
	end
	local timeout
	local getTo = type(to) == 'function' and to or function() return to end
	local cutoff = math.abs(getTo() - from) * 0.01
	function tick()
		from = from + ((getTo() - from) * speed)
		local is_end = math.abs(getTo() - from) <= cutoff
		setter(is_end and getTo() or from)
		request_render()
		if is_end then
			call_me_maybe(callback)
		else
			timeout:resume()
		end
	end
	timeout = mp.add_timeout(0.016, tick)
	tick()
	return function()
		timeout:kill()
		call_me_maybe(callback)
	end
end

-- Kills ongoing animation if one is already running on this element.
-- Killed animation will not get its `on_end` called.
function tween_element(element, from, to, setter, speed, callback)
	if type(speed) ~= 'number' then
		callback = speed
		speed = 0.3
	end

	tween_element_stop(element)

	element.stop_current_animation = tween(
		from, to,
		function(value) setter(element, value) end,
		speed,
		function()
			element.stop_current_animation = nil
			call_me_maybe(callback, element)
		end
	)
end

-- Stopped animation will not get its on_end called.
function tween_element_is_tweening(element)
	return element and element.stop_current_animation
end

-- Stopped animation will not get its on_end called.
function tween_element_stop(element)
	call_me_maybe(element and element.stop_current_animation)
end

-- Helper to automatically use an element property setter
function tween_element_property(element, prop, from, to, speed, callback)
	tween_element(element, from, to, function(_, value) element[prop] = value end, speed, callback)
end

function get_point_to_rectangle_proximity(point, rect)
	local dx = math.max(rect.ax - point.x, 0, point.x - rect.bx + 1)
	local dy = math.max(rect.ay - point.y, 0, point.y - rect.by + 1)
	return math.sqrt(dx*dx + dy*dy);
end

function text_width_estimate(letters, font_size)
	return letters and letters * font_size * options.font_height_to_letter_width_ratio or 0
end

function opacity_to_alpha(opacity)
	return 255 - math.ceil(255 * opacity)
end

function ass_opacity(opacity, fraction)
	fraction = fraction ~= nil and fraction or 1
	if type(opacity) == 'number' then
		return string.format('{\\alpha&H%X&}', opacity_to_alpha(opacity * fraction))
	else
		return string.format(
			'{\\1a&H%X&\\2a&H%X&\\3a&H%X&\\4a&H%X&}',
			opacity_to_alpha((opacity[1] or 0) * fraction),
			opacity_to_alpha((opacity[2] or 0) * fraction),
			opacity_to_alpha((opacity[3] or 0) * fraction),
			opacity_to_alpha((opacity[4] or 0) * fraction)
		)
	end
end

-- Ensures path is absolute and normalizes slashes to the current platform
function normalize_path(path)
	if not path or is_protocol(path) then return path end

	-- Ensure path is absolute
	if not (path:match('^/') or path:match('^%a+:') or path:match('^\\\\')) then
		path = utils.join_path(state.cwd, path)
	end

	-- Use proper slashes
	if state.os == 'windows' then
		return path:gsub('/', '\\')
	else
		return path:gsub('\\', '/')
	end
end

-- Check if path is a protocol, such as `http://...`
function is_protocol(path)
	return path:match('^%a[%a%d-_]+://')
end

function get_extension(path)
	local parts = split(path, '%.')
	return parts and #parts > 1 and parts[#parts] or nil
end

-- Serializes path into its semantic parts
function serialize_path(path)
	if not path or is_protocol(path) then return end
	path = normalize_path(path)
	local parts = split(path, '[\\/]+')
	if parts[#parts] == '' then table.remove(parts, #parts) end -- remove trailing separator
	local basename = parts and parts[#parts] or path
	local dirname = #parts > 1 and table.concat(itable_slice(parts, 1, #parts - 1), state.os == 'windows' and '\\' or '/') or nil
	local dot_split = split(basename, '%.')
	return {
		path = path:sub(-1) == ':' and state.os == 'windows' and path..'\\' or path,
		is_root = dirname == nil,
		dirname = dirname,
		basename = basename,
		filename = #dot_split > 1 and table.concat(itable_slice(dot_split, 1, #dot_split - 1), '.') or basename,
		extension = #dot_split > 1 and dot_split[#dot_split] or nil,
	}
end

function get_files_in_directory(directory, allowed_types)
	local files, error = utils.readdir(directory, 'files')

	if not files then
		msg.error('Retrieving files failed: '..(error or ''))
		return
	end

	-- Filter only requested file types
	if allowed_types then
		files = itable_filter(files, function(_, file)
			local extension = get_extension(file)
			return extension and itable_find(allowed_types, extension:lower())
		end)
	end

	table.sort(files, word_order_comparator)

	return files
end

function get_adjacent_file(file_path, direction, allowed_types)
	local current_file = serialize_path(file_path)
	local files = get_files_in_directory(current_file.dirname, allowed_types)

	if not files then return end

	for index, file in ipairs(files) do
		if current_file.basename == file then
			if direction == 'forward' then
				if files[index + 1] then return utils.join_path(current_file.dirname, files[index + 1]) end
				if options.directory_navigation_loops and files[1] then return utils.join_path(current_file.dirname, files[1]) end
			else
				if files[index - 1] then return utils.join_path(current_file.dirname, files[index - 1]) end
				if options.directory_navigation_loops and files[#files] then return utils.join_path(current_file.dirname, files[#files]) end
			end

			-- This is the only file in directory
			return nil
		end
	end
end

-- Can't use `os.remove()` as it fails on paths with unicode characters.
-- Returns `result, error`, result is table of `status:number(<0=error), stdout, stderr, error_string, killed_by_us:boolean`
function delete_file(file_path)
	local args = state.os == 'windows' and {'cmd', '/C', 'del', file_path} or {'rm', file_path}
	return mp.command_native({name = 'subprocess', args = args, playback_only = false, capture_stdout = true, capture_stderr = true})
end

-- Ensures chapters are in chronological order
function get_normalized_chapters()
	local chapters = mp.get_property_native('chapter-list')

	if not chapters then return end

	-- Copy table
	chapters = itable_slice(chapters)

	-- Ensure chronological order of chapters
	table.sort(chapters, function(a, b) return a.time < b.time end)

	return chapters
end

function is_element_persistent(name)
	local option_name = name..'_persistency';
	return (options[option_name].audio and state.is_audio) or (options[option_name].paused and state.pause)
end

-- Element
--[[
Signature:
{
	-- element rectangle coordinates
	ax = 0, ay = 0, bx = 0, by = 0,
	-- cursor<>element relative proximity as a 0-1 floating number
	-- where 0 = completely away, and 1 = touching/hovering
	-- so it's easy to work with and throw into equations
	proximity = 0,
	-- raw cursor<>element proximity in pixels
	proximity_raw = infinity,
	-- called when element is created
	?init = function(this),
	-- called manually when disposing of element
	?destroy = function(this),
	-- triggered when event happens and cursor is above element
	?on_{event_name} = function(this),
	-- triggered when any event happens anywhere on a page
	?on_global_{event_name} = function(this),
	-- object
	?render = function(this_element),
}
]]
local Element = {
	ax = 0, ay = 0, bx = 0, by = 0,
	proximity = 0, proximity_raw = infinity,
}
Element.__index = Element

function Element.new(props)
	local element = setmetatable(props, Element)
	element._eventListeners = {}

	-- Flash timer
	element._flash_out_timer = mp.add_timeout(options.flash_duration / 1000, function()
		local getTo = function() return element.proximity end
		element:tween_property('forced_proximity', 1, getTo, function()
			element.forced_proximity = nil
		end)
	end)
	element._flash_out_timer:kill()

	element:init()

	return element
end

function Element:init() end
function Element:destroy() end

-- Call method if it exists
function Element:maybe(name, ...)
	if self[name] then return self[name](self, ...) end
end

-- Tween helpers
function Element:tween(...) tween_element(self, ...) end
function Element:tween_property(...) tween_element_property(self, ...) end
function Element:tween_stop() tween_element_stop(self) end
function Element:is_tweening() tween_element_is_tweening(self) end

-- Event listeners
function Element:on(name, handler)
	if self._eventListeners[name] == nil then self._eventListeners[name] = {} end
	local preexistingIndex = itable_find(self._eventListeners[name], handler)
	if preexistingIndex then
		return
	else
		self._eventListeners[name][#self._eventListeners[name] + 1] = handler
	end
end
function Element:off(name, handler)
	if self._eventListeners[name] == nil then return end
	local index = itable_find(self._eventListeners, handler)
	if index then table.remove(self._eventListeners, index) end
end
function Element:trigger(name, ...)
	self:maybe('on_'..name, ...)
	if self._eventListeners[name] == nil then return end
	for _, handler in ipairs(self._eventListeners[name]) do handler(...) end
	request_render()
end

-- Briefly flashes the element for `options.flash_duration` milliseconds.
-- Useful to visualize changes of volume and timeline when changed via hotkeys.
-- Implemented by briefly adding animated `forced_proximity` property to the element.
function Element:flash()
	if options.flash_duration > 0 and (self.proximity < 1 or self._flash_out_timer:is_enabled()) then
		self:tween_stop()
		self.forced_proximity = 1
		self._flash_out_timer:kill()
		self._flash_out_timer:resume()
	end
end

-- ELEMENTS

local Elements = {itable = {}}
Elements.__index = Elements
local elements = setmetatable({}, Elements)

function Elements:add(name, element)
	local insert_index = #Elements.itable + 1

	-- Replace if element already exists
	if self:has(name) then
		insert_index = itable_find(Elements.itable, function(_, element)
			return element.name == name
		end)
	end

	element.name = name
	Elements.itable[insert_index] = element
	self[name] = element

	request_render()
end

function Elements:remove(name, props)
	Elements.itable = itable_remove(Elements.itable, self[name])
	self[name] = nil
	request_render()
end

function Elements:trigger(name, ...)
	for _, element in self:ipairs() do element:trigger(name, ...) end
end

function Elements:has(name) return self[name] ~= nil end
function Elements:ipairs() return ipairs(Elements.itable) end
function Elements:pairs(elements) return pairs(self) end

-- MENU
--[[
Usage:
```
local items = {
	{title = 'Foo title', hint = 'Ctrl+F', value = 'foo'},
	{title = 'Bar title', hint = 'Ctrl+B', value = 'bar'},
	{
		title = 'Submenu',
		items = {
			{title = 'Sub item 1', value = 'sub1'},
			{title = 'Sub item 2', value = 'sub2'}
		}
	}
}

function open_item(value)
	value -- value from `item.value`
end

menu:open(items, open_item)
```
]]
local Menu = {}
Menu.__index = Menu
local menu = setmetatable({key_bindings = {}, is_closing = false}, Menu)

function Menu:is_open(menu_type)
	return elements.menu ~= nil and (not menu_type or elements.menu.type == menu_type)
end

function Menu:open(items, open_item, opts)
	opts = opts or {}

	if menu:is_open() then
		if not opts.parent_menu then
			menu:close(true, function()
				menu:open(items, open_item, opts)
			end)
			return
		end
	else
		menu:enable_key_bindings()
		elements.curtain:fadein()
	end

	elements:add('menu', Element.new({
		type = nil, -- menu type such as `menu`, `chapters`, ...
		title = nil,
		width = nil,
		height = nil,
		offset_x = 0, -- used to animated from/to left when submenu
		item_height = nil,
		item_spacing = 1,
		item_content_spacing = nil,
		font_size = nil,
		scroll_step = nil,
		scroll_height = nil,
		scroll_y = 0,
		opacity = 0,
		relative_parent_opacity = 0.4,
		items = items,
		active_item = nil,
		selected_item = nil,
		open_item = open_item,
		parent_menu = nil,
		init = function(this)
			-- Already initialized
			if this.width ~= nil then return end

			-- Apply options
			for key, value in pairs(opts) do this[key] = value end

			if not this.selected_item then
				this.selected_item = this.active_item
			end

			-- Set initial dimensions
			this:on_display_change()

			-- Scroll to active item
			this:scroll_to_item(this.active_item)

			-- Transition in animation
			menu.transition = {to = 'child', target = this}
			local start_offset = this.parent_menu and (this.parent_menu.width + this.width) / 2 or 0

			tween_element(menu.transition.target, 0, 1, function(_, pos)
				this:set_offset_x(round(start_offset * (1 - pos)))
				this.opacity = pos
				this:set_parent_opacity(1 - ((1 - config.menu_parent_opacity) * pos))
			end, function()
				menu.transition = nil
				update_proximities()
			end)
		end,
		destroy = function(this)
			request_render()
		end,
		on_display_change = function(this)
			this.item_height = state.fullormaxed and options.menu_item_height_fullscreen or options.menu_item_height
			this.font_size = round(this.item_height * 0.48 * options.menu_font_scale)
			this.item_content_spacing = round((this.item_height - this.font_size) * 0.6)
			this.scroll_step = this.item_height + this.item_spacing

			-- Estimate width of a widest item
			local estimated_max_width = 0
			for _, item in ipairs(this.items) do
				local item_text_length = ((item.title and item.title:len() or 0) + (item.hint and item.hint:len() or 0))
				local spacings_in_item = item.hint and 3 or 2
				local estimated_width = text_width_estimate(item_text_length, this.font_size) + (this.item_content_spacing * spacings_in_item)
				if estimated_width > estimated_max_width then
					estimated_max_width = estimated_width
				end
			end

			-- Also check menu title
			local menu_title_length = this.title and this.title:len() or 0
			local estimated_menu_title_width = text_width_estimate(menu_title_length, this.font_size)
			if estimated_menu_title_width > estimated_max_width then
				estimated_max_width = estimated_menu_title_width
			end

			-- Coordinates and sizes are of the scrollable area to make
			-- consuming values in rendering easier. Title drawn above this, so
			-- we need to account for that in max_height and ay position.
			this.width = round(math.min(math.max(estimated_max_width, config.menu_min_width), display.width * 0.9))
			local title_height = this.title and this.scroll_step or 0
			local max_height = round(display.height * 0.9) - title_height
			this.height = math.min(round(this.scroll_step * #this.items) - this.item_spacing, max_height)
			this.scroll_height = math.max((this.scroll_step * #this.items) - this.height - this.item_spacing, 0)
			this.ax = round((display.width - this.width) / 2) + this.offset_x
			this.ay = round((display.height - this.height) / 2 + (title_height / 2))
			this.bx = round(this.ax + this.width)
			this.by = round(this.ay + this.height)

			if this.parent_menu then
				this.parent_menu:on_display_change()
			end
		end,
		update = function(this, props)
			if props then
				for key, value in pairs(props) do this[key] = value end
			end

			-- Reset indexes and scroll
			this:select_index(this.selected_item)
			this:activate_index(this.active_item)
			this:scroll_to(this.scroll_y)

			-- Trigger changes and re-render
			this:on_display_change()
			request_render()
		end,
		set_offset_x = function(this, offset)
			local delta = offset - this.offset_x
			this.offset_x = offset
			this.ax = this.ax + delta
			this.bx = this.bx + delta
			if this.parent_menu then
				this.parent_menu:set_offset_x(offset - ((this.width + this.parent_menu.width) / 2) - this.item_spacing)
			else
				update_proximities()
			end
		end,
		fadeout = function(this, callback)
			this:tween(1, 0, function(this, pos)
				this.opacity = pos
				this:set_parent_opacity(pos * config.menu_parent_opacity)
			end, callback)
		end,
		set_parent_opacity = function(this, opacity)
			if this.parent_menu then
				this.parent_menu.opacity = opacity
				this.parent_menu:set_parent_opacity(opacity * config.menu_parent_opacity)
			end
		end,
		get_item_index_below_cursor = function(this)
			return math.ceil((cursor.y - this.ay + this.scroll_y) / this.scroll_step)
		end,
		get_first_visible_index = function(this)
			return round(this.scroll_y / this.scroll_step) + 1
		end,
		get_last_visible_index = function(this)
			return round((this.scroll_y + this.height) / this.scroll_step)
		end,
		get_centermost_visible_index = function(this)
			return round((this.scroll_y + (this.height / 2)) / this.scroll_step)
		end,
		scroll_to = function(this, pos)
			this.scroll_y = math.max(math.min(pos, this.scroll_height), 0)
			request_render()
		end,
		scroll_to_item = function(this, index)
			if (index and index >= 1 and index <= #this.items) then
				this:scroll_to(round((this.scroll_step * (index - 1)) - ((this.height - this.scroll_step) / 2)))
			end
		end,
		select_index = function(this, index)
			this.selected_item = (index and index >= 1 and index <= #this.items) and index or nil
			request_render()
		end,
		select_value = function(this, value)
			this:select_index(itable_find(this.items, function(_, item) return item.value == value end))
		end,
		activate_index = function(this, index)
			this.active_item = (index and index >= 1 and index <= #this.items) and index or nil
			request_render()
		end,
		activate_value = function(this, value)
			this:activate_index(itable_find(this.items, function(_, item) return item.value == value end))
		end,
		delete_index = function(this, index)
			if (index and index >= 1 and index <= #this.items) then
				local previous_active_value = this.active_index and this.items[this.active_index].value or nil
				table.remove(this.items, index)
				this:on_display_change()
				if previous_active_value then this:activate_value(previous_active_value) end
				this:scroll_to_item(this.selected_item)
			end
		end,
		delete_value = function(this, value)
			this:delete_index(itable_find(this.items, function(_, item) return item.value == value end))
		end,
		prev = function(this)
			local default_anchor = this.scroll_height > this.scroll_step and this:get_centermost_visible_index() or this:get_last_visible_index()
			local current_index = this.selected_item or default_anchor + 1
			this.selected_item = math.max(current_index - 1, 1)
			this:scroll_to_item(this.selected_item)
		end,
		next = function(this)
			local default_anchor = this.scroll_height > this.scroll_step and this:get_centermost_visible_index() or this:get_first_visible_index()
			local current_index = this.selected_item or default_anchor - 1
			this.selected_item = math.min(current_index + 1, #this.items)
			this:scroll_to_item(this.selected_item)
		end,
		back = function(this)
			if menu.transition then
				local transition_target = menu.transition.target
				local transition_target_type = menu.transition.target
				tween_element_stop(transition_target)
				if transition_target_type == 'parent' then
					elements:add('menu', transition_target)
				end
				menu.transition = nil
				transition_target:back()
				return
			else
				menu.transition = {to = 'parent', target = this.parent_menu}
			end

			if menu.transition.target == nil then
				menu:close()
				return
			end

			local target = menu.transition.target
			local to_offset = -target.offset_x + this.offset_x

			tween_element(target, 0, 1, function(_, pos)
				this:set_offset_x(round(to_offset * pos))
				this.opacity = 1 - pos
				this:set_parent_opacity(config.menu_parent_opacity + ((1 - config.menu_parent_opacity) * pos))
			end, function()
				menu.transition = nil
				elements:add('menu', target)
				update_proximities()
			end)
		end,
		open_selected_item = function(this, soft)
			-- If there is a transition active and this method got called, it
			-- means we are animating from this menu to parent menu, and all
			-- calls to this method should be relayed to the parent menu.
			if menu.transition and menu.transition.to == 'parent' then
				local target = menu.transition.target
				tween_element_stop(target)
				menu.transition = nil
				target:open_selected_item(soft)
				return
			end

			if this.selected_item then
				local item = this.items[this.selected_item]
				-- Is submenu
				if item.items then
					local opts = table_copy(opts)
					opts.parent_menu = this
					menu:open(item.items, this.open_item, opts)
				else
					if soft ~= true then menu:close(true) end
					this.open_item(item.value)
				end
			end
		end,
		open_selected_item_soft = function(this) this:open_selected_item(true) end,
		close = function(this) menu:close() end,
		on_global_mbtn_left_down = function(this)
			if this.proximity_raw == 0 then
				this.selected_item = this:get_item_index_below_cursor()
				this:open_selected_item()
			else
				-- check if this is clicking on any parent menus
				local parent_menu = this.parent_menu
				repeat
					if parent_menu then
						if get_point_to_rectangle_proximity(cursor, parent_menu) == 0 then
							this:back()
							return
						end
						parent_menu = parent_menu.parent_menu
					end
				until parent_menu == nil

				menu:close()
			end
		end,
		on_global_mouse_move = function(this)
			if this.proximity_raw == 0 then
				this.selected_item = this:get_item_index_below_cursor()
			else
				if this.selected_item then this.selected_item = nil end
			end
			request_render()
		end,
		on_wheel_up = function(this)
			this.selected_item = nil
			this:scroll_to(this.scroll_y - this.scroll_step)
			-- Selects item below cursor
			this:on_global_mouse_move()
			request_render()
		end,
		on_wheel_down = function(this)
			this.selected_item = nil
			this:scroll_to(this.scroll_y + this.scroll_step)
			-- Selects item below cursor
			this:on_global_mouse_move()
			request_render()
		end,
		on_pgup = function(this)
			this.selected_item = nil
			this:scroll_to(this.scroll_y - this.height)
		end,
		on_pgdwn = function(this)
			this.selected_item = nil
			this:scroll_to(this.scroll_y + this.height)
		end,
		on_home = function(this)
			this.selected_item = nil
			this:scroll_to(0)
		end,
		on_end = function(this)
			this.selected_item = nil
			this:scroll_to(this.scroll_height)
		end,
		render = render_menu,
	}))

	elements.menu:maybe('on_open')
end

function Menu:add_key_binding(key, name, fn, flags)
	menu.key_bindings[#menu.key_bindings + 1] = name
	mp.add_forced_key_binding(key, name, fn, flags)
end

function Menu:enable_key_bindings()
	menu.key_bindings = {}
	-- The `mp.set_key_bindings()` method would be easier here, but that
	-- doesn't support 'repeatable' flag, so we are stuck with this monster.
	menu:add_key_binding('up',              'menu-prev1',        self:create_action('prev'), 'repeatable')
	menu:add_key_binding('down',            'menu-next1',        self:create_action('next'), 'repeatable')
	menu:add_key_binding('left',            'menu-back1',        self:create_action('back'))
	menu:add_key_binding('right',           'menu-select1',      self:create_action('open_selected_item'))
	menu:add_key_binding('shift+right',     'menu-select-soft1', self:create_action('open_selected_item_soft'))
	menu:add_key_binding('shift+mbtn_left', 'menu-select-soft',  self:create_action('open_selected_item_soft'))

	if options.menu_wasd_navigation then
		menu:add_key_binding('w',       'menu-prev2',        self:create_action('prev'), 'repeatable')
		menu:add_key_binding('a',       'menu-back2',        self:create_action('back'))
		menu:add_key_binding('s',       'menu-next2',        self:create_action('next'), 'repeatable')
		menu:add_key_binding('d',       'menu-select2',      self:create_action('open_selected_item'))
		menu:add_key_binding('shift+d', 'menu-select-soft2', self:create_action('open_selected_item_soft'))
	end

	if options.menu_hjkl_navigation then
		menu:add_key_binding('h',       'menu-back3',        self:create_action('back'))
		menu:add_key_binding('j',       'menu-next3',        self:create_action('next'), 'repeatable')
		menu:add_key_binding('k',       'menu-prev3',        self:create_action('prev'), 'repeatable')
		menu:add_key_binding('l',       'menu-select3',      self:create_action('open_selected_item'))
		menu:add_key_binding('shift+l', 'menu-select-soft3', self:create_action('open_selected_item_soft'))
	end

	menu:add_key_binding('mbtn_back',  'menu-back-alt3',   self:create_action('back'))
	menu:add_key_binding('bs',         'menu-back-alt4',   self:create_action('back'))
	menu:add_key_binding('enter',      'menu-select-alt3', self:create_action('open_selected_item'))
	menu:add_key_binding('kp_enter',   'menu-select-alt4', self:create_action('open_selected_item'))
	menu:add_key_binding('esc',        'menu-close',       self:create_action('close'))
	menu:add_key_binding('pgup',       'menu-page-up',     self:create_action('on_pgup'))
	menu:add_key_binding('pgdwn',      'menu-page-down',   self:create_action('on_pgdwn'))
	menu:add_key_binding('home',       'menu-home',        self:create_action('on_home'))
	menu:add_key_binding('end',        'menu-end',         self:create_action('on_end'))
end

function Menu:disable_key_bindings()
	for _, name in ipairs(menu.key_bindings) do mp.remove_key_binding(name) end
	menu.key_bindings = {}
end

function Menu:create_action(name)
	return function(...)
		if elements.menu then elements.menu:maybe(name, ...) end
	end
end

function Menu:close(immediate, callback)
	if type(immediate) ~= 'boolean' then callback = immediate end

	if elements:has('menu') and not menu.is_closing then
		function close()
			elements.menu:maybe('on_close')
			elements.menu:destroy()
			elements:remove('menu')
			menu.is_closing = false
			update_proximities()
			menu:disable_key_bindings()
			call_me_maybe(callback)
		end

		menu.is_closing = true
		elements.curtain:fadeout()

		if immediate then
			close()
		else
			elements.menu:fadeout(close)
		end
	end
end

-- ICONS
--[[
ASS \shadN shadows are drawn also below the element, which when there is an
opacity in play, blends icon colors into ugly greys. The mess below is an
attempt to fix it by rendering shadows for icons with clipping.

Add icons by adding functions to render them to `icons` table.

Signature: function(pos_x, pos_y, size) => string

Function has to return ass path coordinates to draw the icon centered at pox_x
and pos_y of passed size.
]]
local icons = {}
function icon(name, icon_x, icon_y, icon_size, shad_x, shad_y, shad_size, backdrop, opacity, clip)
	local ass = assdraw.ass_new()
	local icon_path = icons[name](icon_x, icon_y, icon_size)
	local icon_color = options['color_'..backdrop..'_text']
	local shad_color = options['color_'..backdrop]
	local use_border = (shad_x + shad_y) == 0
	local icon_border = use_border and shad_size or 0

	-- clip can't clip out shadows, a very annoying limitation I can't work
	-- around without going back to ugly default ass shadows, but atm I actually
	-- don't need clipping of icons with shadows, so I'm choosing to ignore this
	if not clip then
		clip = ''
	end

	if not use_border then
		ass:new_event()
		ass:append('{\\blur0\\bord0\\shad0\\1c&H'..shad_color..'\\iclip('..ass.scale..', '..icon_path..')}')
		ass:append(ass_opacity(opacity))
		ass:pos(shad_x + shad_size, shad_y + shad_size)
		ass:draw_start()
		ass:append(icon_path)
		ass:draw_stop()
	end

	ass:new_event()
	ass:append('{\\blur0\\bord'..icon_border..'\\shad0\\1c&H'..icon_color..'\\3c&H'..shad_color..clip..'}')
	ass:append(ass_opacity(opacity))
	ass:pos(0, 0)
	ass:draw_start()
	ass:append(icon_path)
	ass:draw_stop()

	return ass.text
end

function icons._volume(muted, pos_x, pos_y, size)
	local ass = assdraw.ass_new()
	local scale = size / 200
	function x(number) return pos_x + (number * scale) end
	function y(number) return pos_y + (number * scale) end
	ass:move_to(x(-85), y(-35))
	ass:line_to(x(-50), y(-35))
	ass:line_to(x(-5), y(-75))
	ass:line_to(x(-5), y(75))
	ass:line_to(x(-50), y(35))
	ass:line_to(x(-85), y(35))
	if muted then
		ass:move_to(x(76), y(-35)) ass:line_to(x(50), y(-9)) ass:line_to(x(24), y(-35))
		ass:line_to(x(15), y(-26)) ass:line_to(x(41), y(0)) ass:line_to(x(15), y(26))
		ass:line_to(x(24), y(35)) ass:line_to(x(50), y(9)) ass:line_to(x(76), y(35))
		ass:line_to(x(85), y(26)) ass:line_to(x(59), y(0)) ass:line_to(x(85), y(-26))
	else
		ass:move_to(x(20), y(-30)) ass:line_to(x(20), y(30))
		ass:line_to(x(35), y(30)) ass:line_to(x(35), y(-30))

		ass:move_to(x(55), y(-60)) ass:line_to(x(55), y(60))
		ass:line_to(x(70), y(60)) ass:line_to(x(70), y(-60))
	end
	return ass.text
end
function icons.volume(pos_x, pos_y, size) return icons._volume(false, pos_x, pos_y, size) end
function icons.volume_muted(pos_x, pos_y, size) return icons._volume(true, pos_x, pos_y, size) end

function icons.menu_button(pos_x, pos_y, size)
	local ass = assdraw.ass_new()
	local scale = size / 100
	function x(number) return pos_x + (number * scale) end
	function y(number) return pos_y + (number * scale) end
	local line_height = 14
	local line_spacing = 18
	for i = -1, 1 do
	local offs = i * (line_height + line_spacing)
		ass:move_to(x(-50), y(offs - line_height/2))
		ass:line_to(x(50), y(offs - line_height/2))
		ass:line_to(x(50), y(offs + line_height/2))
		ass:line_to(x(-50), y(offs + line_height/2))
	end
	return ass.text
end

function icons.arrow_right(pos_x, pos_y, size)
	local ass = assdraw.ass_new()
	local scale = size / 200
	function x(number) return pos_x + (number * scale) end
	function y(number) return pos_y + (number * scale) end
	ass:move_to(x(-22), y(-80))
	ass:line_to(x(-45), y(-57))
	ass:line_to(x(12), y(0))
	ass:line_to(x(-45), y(57))
	ass:line_to(x(-22), y(80))
	ass:line_to(x(58), y(0))
	return ass.text
end

-- STATE UPDATES

function update_display_dimensions()
	local o = mp.get_property_native('osd-dimensions')
	display.width = o.w
	display.height = o.h
	display.aspect = o.aspect

	-- Tell elements about this
	elements:trigger('display_change')

	-- Some elements probably changed their rectangles as a reaction to `display_change`
	update_proximities()
	request_render()
end

function update_element_cursor_proximity(element)
	if cursor.hidden then
		element.proximity_raw = infinity
		element.proximity = 0
	else
		local range = options.proximity_out - options.proximity_in
		element.proximity_raw = get_point_to_rectangle_proximity(cursor, element)
		element.proximity = menu:is_open() and 0 or 1 - (math.min(math.max(element.proximity_raw - options.proximity_in, 0), range) / range)
	end
end

function update_proximities()
	local capture_mbtn_left = false
	local capture_wheel = false
	local menu_only = menu:is_open()
	local mouse_leave_elements = {}
	local mouse_enter_elements = {}

	-- Calculates proximities and opacities for defined elements
	for _, element in elements:ipairs() do
		local previous_proximity_raw = element.proximity_raw

		-- If menu is open, all other elements have to be disabled
		if menu_only then
			if element.name == 'menu' then
				capture_mbtn_left = true
				capture_wheel = true
				update_element_cursor_proximity(element)
			else
				element.proximity_raw = infinity
				element.proximity = 0
			end
		else
			update_element_cursor_proximity(element)
		end

		-- Element has global forced key listeners
		if element.on_global_mbtn_left_down then capture_mbtn_left = true end
		if element.on_global_wheel_up or element.on_global_wheel_down then capture_wheel = true end

		if element.proximity_raw == 0 then
			-- Element has local forced key listeners
			if element.on_mbtn_left_down then capture_mbtn_left = true end
			if element.on_wheel_up or element.on_wheel_up then capture_wheel = true end

			-- Mouse entered element area
			if previous_proximity_raw ~= 0 then
				mouse_enter_elements[#mouse_enter_elements + 1] = element
			end
		else
			-- Mouse left element area
			if previous_proximity_raw == 0 then
				mouse_leave_elements[#mouse_leave_elements + 1] = element
			end
		end
	end

	-- Enable key group captures elements request.
	if capture_mbtn_left then
		forced_key_bindings.mbtn_left:enable()
	else
		forced_key_bindings.mbtn_left:disable()
	end
	if capture_wheel then
		forced_key_bindings.wheel:enable()
	else
		forced_key_bindings.wheel:disable()
	end

	-- Trigger `mouse_leave` and `mouse_enter` events
	for _, element in ipairs(mouse_leave_elements) do element:trigger('mouse_leave') end
	for _, element in ipairs(mouse_enter_elements) do element:trigger('mouse_enter') end
end

-- ELEMENT RENDERERS

function render_timeline(this)
	if this.size_max == 0 or state.duration == nil or state.duration == 0 or state.position == nil then return end

	local size_min = this:get_effective_size_min()
	local size = this:get_effective_size()

	if size < 1 then return end

	local ass = assdraw.ass_new()

	-- Text opacity rapidly drops to 0 just before it starts overflowing, or before it reaches timeline.size_min
	local hide_text_below = math.max(this.font_size * 0.7, size_min * 2)
	local hide_text_ramp = hide_text_below / 2
	local text_opacity = math.max(math.min(size - hide_text_below, hide_text_ramp), 0) / hide_text_ramp

	local spacing = math.max(math.floor((this.size_max - this.font_size) / 2.5), 4)
	local progress = state.position / state.duration

	-- Background bar coordinates
	local bax = this.ax
	local bay = this.by - size
	local bbx = this.bx
	local bby = this.by

	-- Foreground bar coordinates
	local fax = bax
	local fay = bay + this.top_border
	local fbx = fax + this.width * progress
	local fby = bby
	local foreground_size = bby - bay
	local foreground_coordinates = fax..','..fay..','..fbx..','..fby -- for clipping

	-- Background
	ass:new_event()
	ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'\\iclip('..foreground_coordinates..')}')
	ass:append(ass_opacity(math.max(options.timeline_opacity - 0.1, 0)))
	ass:pos(0, 0)
	ass:draw_start()
	ass:rect_cw(bax, bay, bbx, bby)
	ass:draw_stop()

	-- Foreground
	ass:new_event()
	ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
	ass:append(ass_opacity(options.timeline_opacity))
	ass:pos(0, 0)
	ass:draw_start()
	ass:rect_cw(fax, fay, fbx, fby)
	ass:draw_stop()

	-- Seekable ranges
	if options.timeline_cached_ranges and state.cached_ranges then
		local range_height = math.max(foreground_size / 8, size_min)
		local range_ay = fby - range_height
		for _, range in ipairs(state.cached_ranges) do
			ass:new_event()
			ass:append('{\\blur0\\bord0\\1c&H'..options.timeline_cached_ranges.color..'}')
			ass:append(ass_opacity(options.timeline_cached_ranges.opacity))
			ass:pos(0, 0)
			ass:draw_start()
			local range_start = math.max(type(range['start']) == 'number' and range['start'] or 0.000001, 0.000001)
			local range_end = math.min(type(range['end']) and range['end'] or state.duration, state.duration)
			ass:rect_cw(
				bax + this.width * (range_start / state.duration), range_ay,
				bax + this.width * (range_end / state.duration), range_ay + range_height
			)
			ass:draw_stop()
		end
	end

	-- Custom ranges
	if state.chapter_ranges ~= nil then
		for i, chapter_range in ipairs(state.chapter_ranges) do
			for i, range in ipairs(chapter_range.ranges) do
				local rax = bax + this.width * (range['start'].time / state.duration)
				local rbx = bax + this.width * (range['end'].time / state.duration)
				ass:new_event()
				ass:append('{\\blur0\\bord0\\1c&H'..chapter_range.color..'}')
				ass:append(ass_opacity(chapter_range.opacity))
				ass:pos(0, 0)
				ass:draw_start()
				-- for 1px chapter size, use the whole size of the bar including padding
				if size <= 1 then
					ass:rect_cw(rax, bay, rbx, bby)
				else
					ass:rect_cw(rax, fay, rbx, fby)
				end
				ass:draw_stop()
			end
		end
	end

	-- Chapters
	if (
		options.chapters ~= 'none'
		and (
			state.chapters ~= nil and #state.chapters > 0
			or state.ab_loop_a and state.ab_loop_a > 0
			or state.ab_loop_b and state.ab_loop_b > 0
		)
	) then
		local half_size = size / 2
		local dots = false
		local chapter_size, chapter_y
		if options.chapters == 'dots' then
			dots = true
			chapter_size = math.min(6, (foreground_size / 2) + 2)
			chapter_y = math.min(fay + chapter_size, fay + half_size)
		elseif options.chapters == 'lines' then
			chapter_size = size
			chapter_y = fay + (chapter_size / 2)
		elseif options.chapters == 'lines-top' then
			chapter_size = math.min(this.size_max / 3.5, size)
			chapter_y = fay + (chapter_size / 2)
		elseif options.chapters == 'lines-bottom' then
			chapter_size = math.min(this.size_max / 3.5, size)
			chapter_y = fay + size - (chapter_size / 2)
		end

		if chapter_size ~= nil then
			-- for 1px chapter size, use the whole size of the bar including padding
			chapter_size = size <= 1 and foreground_size or chapter_size
			local chapter_half_size = chapter_size / 2
			local draw_chapter = function (time)
				local chapter_x = bax + this.width * (time / state.duration)
				local color = chapter_x > fbx and options.color_foreground or options.color_background

				ass:new_event()
				ass:append('{\\blur0\\bord0\\1c&H'..color..'}')
				ass:append(ass_opacity(options.chapters_opacity))
				ass:pos(0, 0)
				ass:draw_start()

				if dots then
					local bezier_stretch = chapter_size * 0.67
					ass:move_to(chapter_x - chapter_half_size, chapter_y)
					ass:bezier_curve(
						chapter_x - chapter_half_size, chapter_y - bezier_stretch,
						chapter_x + chapter_half_size, chapter_y - bezier_stretch,
						chapter_x + chapter_half_size, chapter_y
					)
					ass:bezier_curve(
						chapter_x + chapter_half_size, chapter_y + bezier_stretch,
						chapter_x - chapter_half_size, chapter_y + bezier_stretch,
						chapter_x - chapter_half_size, chapter_y
					)
				else
					ass:rect_cw(chapter_x, chapter_y - chapter_half_size, chapter_x + 1, chapter_y + chapter_half_size)
				end

				ass:draw_stop()
			end

			if state.chapters ~= nil then
				for i, chapter in ipairs(state.chapters) do
					draw_chapter(chapter.time)
				end
			end

			if state.ab_loop_a and state.ab_loop_a > 0 then
				draw_chapter(state.ab_loop_a)
			end

			if state.ab_loop_b and state.ab_loop_b > 0 then
				draw_chapter(state.ab_loop_b)
			end
		end
	end

	if text_opacity > 0 then
		-- Elapsed time
		if state.elapsed_seconds then
			local elapsed_x = bax + spacing
			local elapsed_y = fay + (size / 2)
			ass:new_event()
			ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..foreground_coordinates..')')
			ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
			ass:pos(elapsed_x, elapsed_y)
			ass:an(4)
			ass:append(state.elapsed_time)
			ass:new_event()
			ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\iclip('..foreground_coordinates..')')
			ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
			ass:pos(elapsed_x, elapsed_y)
			ass:an(4)
			ass:append(state.elapsed_time)
		end

		-- End time
		local end_time
		if options.total_time then
			end_time = this.total_time
		else
			end_time = state.remaining_time and '-'..state.remaining_time
		end
		if end_time then
			local end_x = bbx - spacing
			local end_y = fay + (size / 2)
			ass:new_event()
			ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..foreground_coordinates..')')
			ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
			ass:pos(end_x, end_y)
			ass:an(6)
			ass:append(end_time)
			ass:new_event()
			ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\iclip('..foreground_coordinates..')')
			ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1), text_opacity))
			ass:pos(end_x, end_y)
			ass:an(6)
			ass:append(end_time)
		end
	end

	if (this.proximity_raw == 0 or this.pressed) and not (elements.speed and elements.speed.dragging) then
		-- Hovered time
		local hovered_seconds = state.duration * (cursor.x / display.width)
		local box_half_width_guesstimate = (this.font_size * 4.2) / 2
		ass:new_event()
		ass:append('{\\blur0\\bord1\\shad0\\1c&H'..options.color_background_text..'\\3c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'')
		ass:append(ass_opacity(math.min(options.timeline_opacity + 0.1, 1)))
		ass:pos(math.min(math.max(cursor.x, box_half_width_guesstimate), display.width - box_half_width_guesstimate), fay)
		ass:an(2)
		ass:append(mp.format_time(hovered_seconds))

		-- Cursor line
		ass:new_event()
		ass:append('{\\blur0\\bord0\\xshad-1\\yshad0\\1c&H'..options.color_foreground..'\\4c&H'..options.color_background..'}')
		ass:append(ass_opacity(0.2))
		ass:pos(0, 0)
		ass:draw_start()
		ass:rect_cw(cursor.x, fay, cursor.x + 1, fby)
		ass:draw_stop()
	end

	return ass
end

function render_top_bar(this)
	local opacity = this:get_effective_proximity()

	if not this.enabled or opacity == 0 then return end

	local ass = assdraw.ass_new()

	if options.top_bar_controls then
		-- Close button
		local close = elements.window_controls_close
		if close.proximity_raw == 0 then
			-- Background on hover
			ass:new_event()
			ass:append('{\\blur0\\bord0\\1c&H2311e8}')
			ass:append(ass_opacity(this.button_opacity, opacity))
			ass:pos(0, 0)
			ass:draw_start()
			ass:rect_cw(close.ax, close.ay, close.bx, close.by)
			ass:draw_stop()
		end
		ass:new_event()
		ass:append('{\\blur0\\bord1\\shad1\\3c&HFFFFFF\\4c&H000000}')
		ass:append(ass_opacity(this.button_opacity, opacity))
		ass:pos(close.ax + (this.button_width / 2), close.ay + (this.size / 2))
		ass:draw_start()
		ass:move_to(-this.icon_size, this.icon_size)
		ass:line_to(this.icon_size, -this.icon_size)
		ass:move_to(-this.icon_size, -this.icon_size)
		ass:line_to(this.icon_size, this.icon_size)
		ass:draw_stop()

		-- Maximize button
		local maximize = elements.window_controls_maximize
		if maximize.proximity_raw == 0 then
			-- Background on hover
			ass:new_event()
			ass:append('{\\blur0\\bord0\\1c&H222222}')
			ass:append(ass_opacity(this.button_opacity, opacity))
			ass:pos(0, 0)
			ass:draw_start()
			ass:rect_cw(maximize.ax, maximize.ay, maximize.bx, maximize.by)
			ass:draw_stop()
		end
		ass:new_event()
		ass:append('{\\blur0\\bord2\\shad0\\1c\\3c&H000000}')
		ass:append(ass_opacity({[3] = this.button_opacity}, opacity))
		ass:pos(maximize.ax + (this.button_width / 2), maximize.ay + (this.size / 2))
		ass:draw_start()
		ass:rect_cw(-this.icon_size + 1, -this.icon_size + 1, this.icon_size + 1, this.icon_size + 1)
		ass:draw_stop()
		ass:new_event()
		ass:append('{\\blur0\\bord2\\shad0\\1c\\3c&HFFFFFF}')
		ass:append(ass_opacity({[3] = this.button_opacity}, opacity))
		ass:pos(maximize.ax + (this.button_width / 2), maximize.ay + (this.size / 2))
		ass:draw_start()
		ass:rect_cw(-this.icon_size, -this.icon_size, this.icon_size, this.icon_size)
		ass:draw_stop()

		-- Minimize button
		local minimize = elements.window_controls_minimize
		if minimize.proximity_raw == 0 then
			-- Background on hover
			ass:new_event()
			ass:append('{\\blur0\\bord0\\1c&H222222}')
			ass:append(ass_opacity(this.button_opacity, opacity))
			ass:pos(0, 0)
			ass:draw_start()
			ass:rect_cw(minimize.ax, minimize.ay, minimize.bx, minimize.by)
			ass:draw_stop()
		end
		ass:new_event()
		ass:append('{\\blur0\\bord1\\shad1\\3c&HFFFFFF\\4c&H000000}')
		ass:append(ass_opacity(this.button_opacity, opacity))
		ass:append('{\\1a&HFF&}')
		ass:pos(minimize.ax + (this.button_width / 2), minimize.ay + (this.size / 2))
		ass:draw_start()
		ass:move_to(-this.icon_size, 0)
		ass:line_to(this.icon_size, 0)
		ass:draw_stop()
	end

	-- Window title
	if options.top_bar_title and state.media_title then
		local clip_coordinates = this.ax..','..this.ay..','..(this.title_bx - this.spacing)..','..this.by

		ass:new_event()
		ass:append('{\\q2\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'\\clip('..clip_coordinates..')')
		ass:append(ass_opacity(1, opacity))
		ass:pos(this.ax + this.spacing, this.ay + (this.size / 2))
		ass:an(4)
		ass:append(state.media_title)
	end

	return ass
end

function render_volume(this)
	local slider = elements.volume_slider
	local opacity = this:get_effective_proximity()

	if this.width == 0 or opacity == 0 then return end

	local ass = assdraw.ass_new()

	if slider.height > 0 then
		-- Background bar coordinates
		local bax = slider.ax
		local bay = slider.ay
		local bbx = slider.bx
		local bby = slider.by

		-- Foreground bar coordinates
		local height_without_border = slider.height - (options.volume_border * 2)
		local fax = slider.ax + options.volume_border
		local fay = slider.ay + (height_without_border * (1 - math.min(state.volume / state.volume_max, 1))) + options.volume_border
		local fbx = slider.bx - options.volume_border
		local fby = slider.by - options.volume_border

		-- Path to draw a foreground bar with a 100% volume indicator, already
		-- clipped by volume level. Can't just clip it with rectangle, as it itself
		-- also needs to be used as a path to clip the background bar and volume
		-- number.
		local fpath = assdraw.ass_new()
		fpath:move_to(fbx, fby)
		fpath:line_to(fax, fby)
		local nudge_bottom_y = slider.nudge_y + slider.nudge_size
		if fay <= nudge_bottom_y and slider.draw_nudge then
			fpath:line_to(fax, math.min(nudge_bottom_y))
			if fay <= slider.nudge_y then
				fpath:line_to((fax + slider.nudge_size), slider.nudge_y)
				local nudge_top_y = slider.nudge_y - slider.nudge_size
				if fay <= nudge_top_y then
					fpath:line_to(fax, nudge_top_y)
					fpath:line_to(fax, fay)
					fpath:line_to(fbx, fay)
					fpath:line_to(fbx, nudge_top_y)
				else
					local triangle_side = fay - nudge_top_y
					fpath:line_to((fax + triangle_side), fay)
					fpath:line_to((fbx - triangle_side), fay)
				end
				fpath:line_to((fbx - slider.nudge_size), slider.nudge_y)
			else
				local triangle_side = nudge_bottom_y - fay
				fpath:line_to((fax + triangle_side), fay)
				fpath:line_to((fbx - triangle_side), fay)
			end
			fpath:line_to(fbx, nudge_bottom_y)
		else
			fpath:line_to(fax, fay)
			fpath:line_to(fbx, fay)
		end
		fpath:line_to(fbx, fby)

		-- Background
		ass:new_event()
		ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'\\iclip('..fpath.scale..', '..fpath.text..')}')
		ass:append(ass_opacity(math.max(options.volume_opacity - 0.1, 0), opacity))
		ass:pos(0, 0)
		ass:draw_start()
		ass:move_to(bax, bay)
		ass:line_to(bbx, bay)
		local half_border = options.volume_border / 2
		if slider.draw_nudge then
			ass:line_to(bbx, math.max(slider.nudge_y - slider.nudge_size + half_border, bay))
			ass:line_to(bbx - slider.nudge_size + half_border, slider.nudge_y)
			ass:line_to(bbx, slider.nudge_y + slider.nudge_size - half_border)
		end
		ass:line_to(bbx, bby)
		ass:line_to(bax, bby)
		if slider.draw_nudge then
			ass:line_to(bax, slider.nudge_y + slider.nudge_size - half_border)
			ass:line_to(bax + slider.nudge_size - half_border, slider.nudge_y)
			ass:line_to(bax, math.max(slider.nudge_y - slider.nudge_size + half_border, bay))
		end
		ass:line_to(bax, bay)
		ass:draw_stop()

		-- Foreground
		ass:new_event()
		ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
		ass:append(ass_opacity(options.volume_opacity, opacity))
		ass:pos(0, 0)
		ass:draw_start()
		ass:append(fpath.text)
		ass:draw_stop()

		-- Current volume value
		local volume_string = tostring(round(state.volume * 10) / 10)
		local font_size = round(((this.width * 0.6) - (#volume_string * (this.width / 20))) * options.volume_font_scale)
		if fay < slider.by - slider.spacing then
			ass:new_event()
			ass:append('{\\blur0\\bord0\\shad0\\1c&H'..options.color_foreground_text..'\\fn'..config.font..'\\fs'..font_size..bold_tag..'\\clip('..fpath.scale..', '..fpath.text..')}')
			ass:append(ass_opacity(math.min(options.volume_opacity + 0.1, 1), opacity))
			ass:pos(slider.ax + (slider.width / 2), slider.by - slider.spacing)
			ass:an(2)
			ass:append(volume_string)
		end
		if fay > slider.by - slider.spacing - font_size then
			ass:new_event()
			ass:append('{\\blur0\\bord0\\shad1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..font_size..bold_tag..'\\iclip('..fpath.scale..', '..fpath.text..')}')
			ass:append(ass_opacity(math.min(options.volume_opacity + 0.1, 1), opacity))
			ass:pos(slider.ax + (slider.width / 2), slider.by - slider.spacing)
			ass:an(2)
			ass:append(volume_string)
		end
	end

	-- Mute button
	local mute = elements.volume_mute
	local icon_name = state.mute and 'volume_muted' or 'volume'
	ass:new_event()
	ass:append(icon(
		icon_name,
		mute.ax + (mute.width / 2), mute.ay + (mute.height / 2), mute.width * 0.7, -- x, y, size
		0, 0, options.volume_border, -- shadow_x, shadow_y, shadow_size
		'background', options.volume_opacity * opacity -- backdrop, opacity
	))
	return ass
end

function render_speed(this)
	if not this.dragging and (elements.curtain.opacity > 0) then return end

	local proximity = this:get_effective_proximity()
	local opacity = this.dragging and 1 or proximity

	if opacity == 0 then return end

	local ass = assdraw.ass_new()

	-- Coordinates
	local ax = this.ax
	-- local ay = this.ay + timeline.size_max - timeline:get_effective_size()
	local ay = this.ay
	local bx = this.bx
	local by = ay + this.height
	local half_width = (this.width / 2)
	local half_x = ax + half_width

	-- Notches
	local speed_at_center = state.speed
	if this.dragging then
		speed_at_center = this.dragging.start_speed + ((-this.dragging.distance / this.step_distance) * options.speed_step)
		speed_at_center = math.min(math.max(speed_at_center, 0.01), 100)
	end
	local nearest_notch_speed = round(speed_at_center / this.notch_every) * this.notch_every
	local nearest_notch_x = half_x + (((nearest_notch_speed - speed_at_center) / this.notch_every) * this.notch_spacing)
	local guide_size = math.floor(this.height / 7.5)
	local notch_by = by - guide_size
	local notch_ay_big = ay + round(this.font_size * 1.1)
	local notch_ay_medium = notch_ay_big + ((notch_by - notch_ay_big) * 0.2)
	local notch_ay_small = notch_ay_big + ((notch_by - notch_ay_big) * 0.4)
	local from_to_index = math.floor(this.notches / 2)

	for i = -from_to_index, from_to_index do
		local notch_speed = nearest_notch_speed + (i * this.notch_every)

		if notch_speed < 0 or notch_speed > 100 then goto continue end

		local notch_x = nearest_notch_x + (i * this.notch_spacing)
		local notch_thickness = 1
		local notch_ay = notch_ay_small
		if (notch_speed % (this.notch_every * 10)) < 0.00000001 then
			notch_ay = notch_ay_big
			notch_thickness = 1
		elseif (notch_speed % (this.notch_every * 5)) < 0.00000001 then
			notch_ay = notch_ay_medium
		end

		ass:new_event()
		ass:append('{\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000}')
		ass:append(ass_opacity(math.min(1.2 - (math.abs((notch_x - ax - half_width) / half_width)), 1), opacity))
		ass:pos(0, 0)
		ass:draw_start()
		ass:move_to(notch_x - notch_thickness, notch_ay)
		ass:line_to(notch_x + notch_thickness, notch_ay)
		ass:line_to(notch_x + notch_thickness, notch_by)
		ass:line_to(notch_x - notch_thickness, notch_by)
		ass:draw_stop()

		::continue::
	end

	-- Center guide
	ass:new_event()
	ass:append('{\\blur0\\bord1\\shad0\\1c&HFFFFFF\\3c&H000000}')
	ass:append(ass_opacity(options.speed_opacity, opacity))
	ass:pos(0, 0)
	ass:draw_start()
	ass:move_to(half_x, by - 2 - guide_size)
	ass:line_to(half_x + guide_size, by - 2)
	ass:line_to(half_x - guide_size, by - 2)
	ass:draw_stop()

	-- Speed value
	local speed_text = (round(state.speed * 100) / 100)..'x'
	ass:new_event()
	ass:append('{\\blur0\\bord1\\shad0\\1c&H'..options.color_background_text..'\\3c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..'}')
	ass:append(ass_opacity(options.speed_opacity, opacity))
	ass:pos(half_x, ay)
	ass:an(8)
	ass:append(speed_text)

	return ass
end

function render_menu_button(this)
	local opacity = this:get_effective_proximity()

	if this.width == 0 or opacity == 0 then return end

	if this.proximity_raw > 0 then opacity = opacity / 2 end

	local ass = assdraw.ass_new()
	-- Menu button
	local burger = elements.menu_button
	ass:new_event()
	ass:append(icon(
	'menu_button',
		burger.ax + (burger.width / 2), burger.ay + (burger.height / 2), burger.width, -- x, y, size
		0, 0, options.menu_button_border, -- shadow_x, shadow_y, shadow_size
		'background', options.menu_button_opacity * opacity -- backdrop, opacity
	))
	return ass
end

function render_menu(this)
	local ass = assdraw.ass_new()

	if this.parent_menu then
		ass:merge(this.parent_menu:render())
	end

	-- Menu title
	if this.title then
		-- Background
		ass:new_event()
		ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
		ass:append(ass_opacity(options.menu_opacity, this.opacity))
		ass:pos(0, 0)
		ass:draw_start()
		ass:rect_cw(this.ax, this.ay - this.item_height, this.bx, this.ay - 1)
		ass:draw_stop()

		-- Title
		ass:new_event()
		ass:append('{\\blur0\\bord0\\shad1\\b1\\1c&H'..options.color_background_text..'\\4c&H'..options.color_background..'\\fn'..config.font..'\\fs'..this.font_size..'\\q2\\clip('..this.ax..','..this.ay - this.item_height..','..this.bx..','..this.ay..')}')
		ass:append(ass_opacity(options.menu_opacity, this.opacity))
		ass:pos(display.width / 2, this.ay - (this.item_height * 0.5))
		ass:an(5)
		ass:append(this.title)
	end

	local scroll_area_clip = '\\clip('..this.ax..','..this.ay..','..this.bx..','..this.by..')'

	for index, item in ipairs(this.items) do
		local item_ay = this.ay - this.scroll_y + (this.item_height * (index - 1) + this.item_spacing * (index - 1))
		local item_by = item_ay + this.item_height
		local item_clip = ''

		-- Clip items overflowing scroll area
		if item_ay <= this.ay or item_by >= this.by then
			item_clip = scroll_area_clip
		end

		if item_by < this.ay or item_ay > this.by then goto continue end

		local is_active = this.active_item == index
		local font_color, background_color, ass_shadow, ass_shadow_color
		local icon_size = this.font_size

		if is_active then
			font_color, background_color = options.color_foreground_text, options.color_foreground
			ass_shadow, ass_shadow_color = '\\shad0', ''
		else
			font_color, background_color = options.color_background_text, options.color_background
			ass_shadow, ass_shadow_color = '\\shad1', '\\4c&H'..background_color
		end

		local has_submenu = item.items ~= nil
		local hint_width = 0
		if item.hint then
			hint_width = text_width_estimate(item.hint:len(), this.font_size) + this.item_content_spacing
		elseif has_submenu then
			hint_width = icon_size + this.item_content_spacing
		end

		-- Background
		ass:new_event()
		ass:append('{\\blur0\\bord0\\1c&H'..background_color..item_clip..'}')
		ass:append(ass_opacity(options.menu_opacity, this.opacity))
		ass:pos(0, 0)
		ass:draw_start()
		ass:rect_cw(this.ax, item_ay, this.bx, item_by)
		ass:draw_stop()

		-- Selected highlight
		if this.selected_item == index then
			ass:new_event()
			ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..item_clip..'}')
			ass:append(ass_opacity(0.1, this.opacity))
			ass:pos(0, 0)
			ass:draw_start()
			ass:rect_cw(this.ax, item_ay, this.bx, item_by)
			ass:draw_stop()
		end

		-- Title
		if item.title then
			item.ass_save_title = item.ass_save_title or item.title:gsub("([{}])","\\%1")
			local title_clip_x = (this.bx - hint_width - this.item_content_spacing)
			local title_clip = '\\clip('..this.ax..','..math.max(item_ay, this.ay)..','..title_clip_x..','..math.min(item_by, this.by)..')'
			ass:new_event()
			ass:append('{\\blur0\\bord0\\shad1\\1c&H'..font_color..'\\4c&H'..background_color..'\\fn'..config.font..'\\fs'..this.font_size..bold_tag..title_clip..'\\q2}')
			ass:append(ass_opacity(options.menu_opacity, this.opacity))
			ass:pos(this.ax + this.item_content_spacing, item_ay + (this.item_height / 2))
			ass:an(4)
			ass:append(item.ass_save_title)
		end

		-- Hint
		if item.hint then
			item.ass_save_hint = item.ass_save_hint or item.hint:gsub("([{}])","\\%1")
			ass:new_event()
			ass:append('{\\blur0\\bord0'..ass_shadow..'\\1c&H'..font_color..''..ass_shadow_color..'\\fn'..config.font..'\\fs'..(this.font_size - 1)..bold_tag..item_clip..'}')
			ass:append(ass_opacity(options.menu_opacity * (has_submenu and 1 or 0.5), this.opacity))
			ass:pos(this.bx - this.item_content_spacing, item_ay + (this.item_height / 2))
			ass:an(6)
			ass:append(item.ass_save_hint)
		elseif has_submenu then
			ass:new_event()
			ass:append(icon(
				'arrow_right',
				this.bx - this.item_content_spacing - (icon_size / 2), -- x
				item_ay + (this.item_height / 2), -- y
				icon_size, -- size
				0, 0, 1, -- shadow_x, shadow_y, shadow_size
				is_active and 'foreground' or 'background', this.opacity, -- backdrop, opacity
				item_clip
			))
		end

		::continue::
	end

	-- Scrollbar
	if this.scroll_height > 0 then
		local groove_height = this.height - 2
		local thumb_height = math.max((this.height / (this.scroll_height + this.height)) * groove_height, 40)
		local thumb_y = this.ay + 1 + ((this.scroll_y / this.scroll_height) * (groove_height - thumb_height))
		ass:new_event()
		ass:append('{\\blur0\\bord0\\1c&H'..options.color_foreground..'}')
		ass:append(ass_opacity(options.menu_opacity, this.opacity * 0.8))
		ass:pos(0, 0)
		ass:draw_start()
		ass:rect_cw(this.bx - 3, thumb_y, this.bx - 1, thumb_y + thumb_height)
		ass:draw_stop()
	end

	return ass
end

-- MAIN RENDERING

-- Request that render() is called.
-- The render is then either executed immediately, or rate-limited if it was
-- called a small time ago.
function request_render()
	if state.render_timer == nil then
		state.render_timer = mp.add_timeout(0, render)
	end

	if not state.render_timer:is_enabled() then
		local now = mp.get_time()
		local timeout = config.render_delay - (now - state.render_last_time)
		if timeout < 0 then
			timeout = 0
		end
		state.render_timer.timeout = timeout
		state.render_timer:resume()
	end
end

function render()
	state.render_last_time = mp.get_time()

	-- Actual rendering
	local ass = assdraw.ass_new()

	for _, element in elements.ipairs() do
		local result = element:maybe('render')
		if result then
			ass:new_event()
			ass:merge(result)
		end
	end

	-- submit
	if osd.res_x == display.width and osd.res_y == display.height and osd.data == ass.text then
		return
	end

	osd.res_x = display.width
	osd.res_y = display.height
	osd.data = ass.text
	osd.z = 2000
	osd:update()
end

-- STATIC ELEMENTS

elements:add('window_border', Element.new({
	size = nil, -- set in init
	init = function(this)
		this:update_size();
	end,
	update_size = function(this)
		this.size = options.window_border_size > 0 and not state.fullormaxed and not state.border and options.window_border_size or 0
	end,
	on_prop_border = function(this) this:update_size() end,
	on_prop_fullormaxed = function(this) this:update_size() end,
	render = function(this)
		if this.size > 0 then
			local ass = assdraw.ass_new()
			local clip_coordinates = this.size..','..this.size..','..(display.width - this.size)..','..(display.height - this.size)
			ass:new_event()
			ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'\\iclip('..clip_coordinates..')}')
			ass:append(ass_opacity(options.window_border_opacity))
			ass:pos(0, 0)
			ass:draw_start()
			ass:rect_cw(0, 0, display.width, display.height)
			ass:draw_stop()
			return ass
		end
	end
}))
elements:add('pause_indicator', Element.new({
	base_icon_opacity = options.pause_indicator == 'flash' and 1 or 0.8,
	paused = false,
	type = options.pause_indicator,
	is_manual = options.pause_indicator == 'manual',
	fadeout_requested = false,
	opacity = 0,
	init = function(this)
		local initial_call = true
		mp.observe_property('pause', 'bool', function(_, paused)
			if initial_call then
				initial_call = false
				return
			end

			this.paused = paused

			if options.pause_indicator == 'flash' then
				this:flash()
			elseif options.pause_indicator == 'static' then
				this:decide()
			end
		end)
	end,
	flash = function(this)
		if not this.is_manual and this.type ~= 'flash' then return end
		-- can't wait for pause property event listener to set this, because when this is used inside a binding like:
		-- cycle pause; script-binding uosc/flash-pause-indicator
		-- the pause event is not fired fast enough, and indicator starts rendering with old icon
		this.paused = mp.get_property_native('pause')
		if this.is_manual then this.type = 'flash' end
		this.opacity = 1
		this:tween_property('opacity', 1, 0, 0.15)
	end,
	-- decides whether static indicator should be visible or not
	decide = function(this)
		if not this.is_manual and this.type ~= 'static' then return end
		this.paused = mp.get_property_native('pause') -- see flash() for why this line is necessary
		if this.is_manual then this.type = 'static' end
		this.opacity = this.paused and 1 or 0
		request_render()

		-- works around an mpv race condition bug during pause on windows builds, which cause osd updates to be ignored
		-- .03 was still loosing renders, .04 was fine, but to be safe I added 10ms more
		mp.add_timeout(.05, function() osd:update() end)
	end,
	render = function(this)
		if this.opacity == 0 then return end

		local ass = assdraw.ass_new()
		local is_static = this.type == 'static'

		-- Background fadeout
		if is_static then
			ass:new_event()
			ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
			ass:append(ass_opacity(0.3, this.opacity))
			ass:pos(0, 0)
			ass:draw_start()
			ass:rect_cw(0, 0, display.width, display.height)
			ass:draw_stop()
		end

		-- Icon
		local size = round((math.min(display.width, display.height) * (is_static and 0.20 or 0.15)) / 2)

		size = size + size * (1 - this.opacity)

		if this.paused then
			ass:new_event()
			ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
			ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
			ass:pos(display.width / 2, display.height / 2)
			ass:draw_start()
			ass:rect_cw(-size, -size, -size / 3, size)
			ass:draw_stop()

			ass:new_event()
			ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
			ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
			ass:pos(display.width / 2, display.height / 2)
			ass:draw_start()
			ass:rect_cw(size / 3, -size, size, size)
			ass:draw_stop()
		else
			ass:new_event()
			ass:append('{\\blur0\\bord1\\1c&H'..options.color_foreground..'\\3c&H'..options.color_background..'}')
			ass:append(ass_opacity(this.base_icon_opacity, this.opacity))
			ass:pos(display.width / 2, display.height / 2)
			ass:draw_start()
			ass:move_to(-size * 0.6, -size)
			ass:line_to(size, 0)
			ass:line_to(-size * 0.6, size)
			ass:draw_stop()
		end

		return ass
	end
}))
elements:add('timeline', Element.new({
	pressed = false,
	size_max = 0, size_min = 0, -- set in `on_display_change` handler based on `state.fullormaxed`
	size_min_override = options.timeline_start_hidden and 0 or nil, -- used for toggle-progress command
	font_size = 0, -- calculated in on_display_change
	total_time = nil, -- set in op_prop_duration listener
	top_border = options.timeline_border,
	get_effective_proximity = function(this)
		if this.pressed or is_element_persistent('timeline') then return 1 end
		if this.forced_proximity then return this.forced_proximity end
		return (elements.volume_slider and elements.volume_slider.pressed) and 0 or this.proximity
	end,
	get_effective_size_min = function(this)
		return this.size_min_override or this.size_min
	end,
	get_effective_size = function(this)
		if elements.speed and elements.speed.dragging then return this.size_max end
		local size_min = this:get_effective_size_min()
		return size_min + math.ceil((this.size_max - size_min) * this:get_effective_proximity())
	end,
	update_dimensions = function(this)
		if state.fullormaxed then
			this.size_min = options.timeline_size_min_fullscreen
			this.size_max = options.timeline_size_max_fullscreen
		else
			this.size_min = options.timeline_size_min
			this.size_max = options.timeline_size_max
		end
		this.font_size = math.floor(math.min((this.size_max + 60) * 0.2, this.size_max * 0.96) * options.timeline_font_scale)
		this.ax = elements.window_border.size
		this.ay = display.height - elements.window_border.size - this.size_max - this.top_border
		this.bx = display.width - elements.window_border.size
		this.by = display.height - elements.window_border.size
		this.width = this.bx - this.ax
	end,
	on_prop_border = function(this) this:update_dimensions() end,
	on_prop_fullormaxed = function(this) this:update_dimensions() end,
	on_display_change = function(this) this:update_dimensions() end,
	on_prop_duration = function(this, value)
		this.total_time = value and mp.format_time(value) or nil
	end,
	set_from_cursor = function(this)
		mp.commandv('seek', (((cursor.x - this.ax) / this.width) * 100), 'absolute-percent+exact')
	end,
	on_mbtn_left_down = function(this)
		this.pressed = true
		this:set_from_cursor()
	end,
	on_global_mbtn_left_up = function(this) this.pressed = false end,
	on_global_mouse_leave = function(this) this.pressed = false end,
	on_global_mouse_move = function(this)
		if this.pressed then this:set_from_cursor() end
	end,
	on_wheel_up = function(this)
		if options.timeline_step > 0 then mp.commandv('seek', -options.timeline_step) end
	end,
	on_wheel_down = function(this)
		if options.timeline_step > 0 then mp.commandv('seek', options.timeline_step) end
	end,
	render = render_timeline,
}))
elements:add('top_bar', Element.new({
	button_opacity = 0.8,
	enabled = false,
	get_effective_proximity = function(this)
		if is_element_persistent('top_bar') then return 1 end
		if this.forced_proximity then return this.forced_proximity end
		return (elements.volume_slider and elements.volume_slider.pressed) and 0 or this.proximity
	end,
	decide_enabled = function(this)
		if options.top_bar == 'no-border' then
			this.enabled = not state.border or state.fullormaxed
		elseif options.top_bar == 'always' then
			this.enabled = true
		else
			this.enabled = false
		end
		this.enabled = this.enabled and (options.top_bar_controls or options.top_bar_title)
	end,
	update_dimensions = function(this)
		this.size = state.fullormaxed and options.top_bar_size_fullscreen or options.top_bar_size
		this.icon_size = round(this.size / 8)
		this.spacing = math.ceil(this.size * 0.25)
		this.font_size = math.floor(this.size - (this.spacing * 2))
		this.button_width = round(this.size * 1.15)
		this.ay = elements.window_border.size
		this.bx = display.width - elements.window_border.size
		this.by = this.size + elements.window_border.size
		this.title_bx = this.bx - (options.top_bar_controls and (this.button_width * 3) or 0)
		this.ax = options.top_bar_title and elements.window_border.size or this.title_bx
	end,
	on_prop_border = function(this)
		this:decide_enabled()
		this:update_dimensions()
	end,
	on_prop_fullormaxed = function(this)
		this:decide_enabled()
		this:update_dimensions()
	end,
	on_display_change = function(this) this:update_dimensions() end,
	render = render_top_bar,
}))
if options.top_bar_controls then
	elements:add('window_controls_minimize', Element.new({
		update_dimensions = function(this)
			this.ax = elements.top_bar.bx - (elements.top_bar.button_width * 3)
			this.ay = elements.top_bar.ay
			this.bx = this.ax + elements.top_bar.button_width
			this.by = this.ay + elements.top_bar.size
		end,
		on_prop_border = function(this) this:update_dimensions() end,
		on_display_change = function(this) this:update_dimensions() end,
		on_mbtn_left_down = function() mp.commandv('cycle', 'window-minimized') end
	}))
	elements:add('window_controls_maximize', Element.new({
		update_dimensions = function(this)
			this.ax = elements.top_bar.bx - (elements.top_bar.button_width * 2)
			this.ay = elements.top_bar.ay
			this.bx = this.ax + elements.top_bar.button_width
			this.by = this.ay + elements.top_bar.size
		end,
		on_prop_border = function(this) this:update_dimensions() end,
		on_display_change = function(this) this:update_dimensions() end,
		on_mbtn_left_down = function() mp.commandv('cycle', 'window-maximized') end
	}))
	elements:add('window_controls_close', Element.new({
		update_dimensions = function(this)
			this.ax = elements.top_bar.bx - elements.top_bar.button_width
			this.ay = elements.top_bar.ay
			this.bx = this.ax + elements.top_bar.button_width
			this.by = this.ay + elements.top_bar.size
		end,
		on_prop_border = function(this) this:update_dimensions() end,
		on_display_change = function(this) this:update_dimensions() end,
		on_mbtn_left_down = function() mp.commandv('quit') end
	}))
end
if itable_find({'left', 'right'}, options.volume) then
	elements:add('volume', Element.new({
		width = nil, -- set in `on_display_change` handler based on `state.fullormaxed`
		height = nil, -- set in `on_display_change` handler based on `state.fullormaxed`
		margin = nil, -- set in `on_display_change` handler based on `state.fullormaxed`
		get_effective_proximity = function(this)
			if is_element_persistent('volume') or elements.volume_slider.pressed then return 1 end
			if this.forced_proximity then return this.forced_proximity end
			return elements.timeline.proximity_raw == 0 and 0 or this.proximity
		end,
		update_dimensions = function(this)
			this.width = state.fullormaxed and options.volume_size_fullscreen or options.volume_size
			this.height = round(math.min(this.width * 8, (elements.timeline.ay - elements.top_bar.size) * 0.8))
			-- Don't bother rendering this if too small
			if this.height < (this.width * 2) then
				this.height = 0
			end
			this.margin = (this.width / 2) + elements.window_border.size
			this.ax = round(options.volume == 'left' and this.margin or display.width - this.margin - this.width)
			this.ay = round((display.height - this.height) / 2)
			this.bx = round(this.ax + this.width)
			this.by = round(this.ay + this.height)
		end,
		on_display_change = function(this) this:update_dimensions() end,
		on_prop_border = function(this) this:update_dimensions() end,
		render = render_volume,
	}))
	elements:add('volume_mute', Element.new({
		width = 0,
		height = 0,
		on_display_change = function(this)
			this.width = elements.volume.width
			this.height = this.width
			this.ax = elements.volume.ax
			this.ay = elements.volume.by - this.height
			this.bx = elements.volume.bx
			this.by = elements.volume.by
		end,
		on_mbtn_left_down = function(this) mp.commandv('cycle', 'mute') end
	}))
	elements:add('volume_slider', Element.new({
		pressed = false,
		width = 0,
		height = 0,
		nudge_y = 0, -- vertical position where volume overflows 100
		nudge_size = nil, -- set on resize
		font_size = nil,
		spacing = nil,
		on_display_change = function(this)
			if state.volume_max == nil or state.volume_max == 0 then return end
			this.ax = elements.volume.ax
			this.ay = elements.volume.ay
			this.bx = elements.volume.bx
			this.by = elements.volume_mute.ay
			this.width = this.bx - this.ax
			this.height = this.by - this.ay
			this.nudge_y = this.by - round(this.height * (100 / state.volume_max))
			this.nudge_size = round(elements.volume.width * 0.18)
			this.draw_nudge = this.ay < this.nudge_y
			this.spacing = round(this.width * 0.2)
		end,
		set_from_cursor = function(this)
			local volume_fraction = (this.by - cursor.y - options.volume_border) / (this.height - options.volume_border)
			local new_volume = math.min(math.max(volume_fraction, 0), 1) * state.volume_max
			new_volume = round(new_volume / options.volume_step) * options.volume_step
			if state.volume ~= new_volume then mp.commandv('set', 'volume', math.min(new_volume, state.volume_max)) end
		end,
		on_mbtn_left_down = function(this)
			this.pressed = true
			this:set_from_cursor()
		end,
		on_global_mbtn_left_up = function(this) this.pressed = false end,
		on_global_mouse_leave = function(this) this.pressed = false end,
		on_global_mouse_move = function(this)
			if this.pressed then this:set_from_cursor() end
		end,
		on_wheel_up = function(this)
			local current_rounded_volume = round(state.volume / options.volume_step) * options.volume_step
			mp.commandv('set', 'volume', math.min(current_rounded_volume + options.volume_step, state.volume_max))
		end,
		on_wheel_down = function(this)
			local current_rounded_volume = round(state.volume / options.volume_step) * options.volume_step
			mp.commandv('set', 'volume', math.min(current_rounded_volume - options.volume_step, state.volume_max))
		end,
	}))
end
if itable_find({'center', 'bottom-bar'}, options.menu_button) then
	elements:add('menu_button', Element.new({
		width = 0, height = 0,
		get_effective_proximity = function(this)
			if menu:is_open() then return 0 end
			if is_element_persistent('menu_button') then return 1 end
			if elements.timeline.proximity_raw == 0 then return 0 end
			if this.forced_proximity then return this.forced_proximity end
			if options.menu_button == 'bottom-bar' then
				local timeline_proximity = elements.timeline.forced_proximity or elements.timeline.proximity
				return this.forced_proximity or math[cursor.hidden and 'min' or 'max'](this.proximity, timeline_proximity)
			end
			return this.proximity
		end,
		update_dimensions = function(this)
			this.width = state.fullormaxed and options.menu_button_size_fullscreen or options.menu_button_size
			this.height = this.width

			if options.menu_button == 'bottom-bar' then
				this.ax = 15
				this.bx = this.ax + this.width
				this.by = display.height - 10 - elements.window_border.size - elements.timeline.size_max - elements.timeline.top_border
				this.ay = this.by - this.height
			else
				this.ax = round((display.width - this.width) / 2)
				this.ay = round((display.height - this.height) / 2)
				this.bx = this.ax + this.width
				this.by = this.ay + this.height
			end
		end,
		on_display_change = function(this) this:update_dimensions() end,
		on_prop_border = function(this) this:update_dimensions() end,
		on_mbtn_left_down = function(this)
			if this.proximity_raw == 0 then menu_key_binding() end
		end,
		render = render_menu_button,
	}))
end
if options.speed then
	elements:add('speed', Element.new({
		dragging = nil,
		width = 0,
		height = 0,
		notches = 10,
		notch_every = 0.1,
		step_distance = nil,
		font_size = nil,
		get_effective_proximity = function(this)
			if elements.timeline.proximity_raw == 0 then return 0 end
			if is_element_persistent('speed') then return 1 end
			if this.forced_proximity then return this.forced_proximity end
			local timeline_proximity = elements.timeline.forced_proximity or elements.timeline.proximity
			return this.forced_proximity or math[cursor.hidden and 'min' or 'max'](this.proximity, timeline_proximity)
		end,
		update_dimensions = function(this)
			this.height = state.fullormaxed and options.speed_size_fullscreen or options.speed_size
			this.width = round(this.height * 3.6)
			this.notch_spacing = this.width / this.notches
			this.step_distance = this.notch_spacing * (options.speed_step / this.notch_every)
			this.ax = (display.width - this.width) / 2
			this.by = display.height - elements.window_border.size - elements.timeline.size_max - elements.timeline.top_border
			this.ay = this.by - this.height
			this.bx = this.ax + this.width
			this.font_size = round(this.height * 0.48 * options.speed_font_scale)
		end,
		set_from_cursor = function(this)
			local volume_fraction = (this.by - cursor.y - options.volume_border) / (this.height - options.volume_border)
			local new_volume = math.min(math.max(volume_fraction, 0), 1) * state.volume_max
			new_volume = round(new_volume / options.volume_step) * options.volume_step
			if state.volume ~= new_volume then mp.commandv('set', 'volume', new_volume) end
		end,
		on_prop_border = function(this) this:update_dimensions() end,
		on_display_change = function(this) this:update_dimensions() end,
		on_mbtn_left_down = function(this)
			this:tween_stop() -- Stop and cleanup possible ongoing animations
			this.dragging = {
				start_time = mp.get_time(),
				start_x = cursor.x,
				distance = 0,
				start_speed = state.speed
			}
		end,
		on_global_mouse_move = function(this)
			if not this.dragging then return end

			this.dragging.distance = cursor.x - this.dragging.start_x
			local steps_dragged = round(-this.dragging.distance / this.step_distance)
			local new_speed = this.dragging.start_speed + (steps_dragged * options.speed_step)
			mp.set_property_native('speed', round(new_speed * 100) / 100)
		end,
		on_mbtn_left_up = function(this)
			-- Reset speed on short clicks
			if this.dragging and math.abs(this.dragging.distance) < 6 and mp.get_time() - this.dragging.start_time < 0.15 then
				mp.set_property_native('speed', 1)
			end
		end,
		on_global_mbtn_left_up = function(this)
			if this.dragging and elements.timeline.proximity_raw == 0 then
				this:fadeout()
			end
			this.dragging = nil
			request_render()
		end,
		on_global_mouse_leave = function(this)
			this.dragging = nil
			request_render()
		end,
		on_wheel_up = function(this)
			mp.set_property_native('speed', state.speed - options.speed_step)
		end,
		on_wheel_down = function(this)
			mp.set_property_native('speed', state.speed + options.speed_step)
		end,
		render = render_speed,
	}))
end
elements:add('curtain', Element.new({
	opacity = 0,
	fadeout = function(this)
		this:tween_property('opacity', this.opacity, 0);
	end,
	fadein = function(this)
		this:tween_property('opacity', this.opacity, 1);
	end,
	render = function(this)
		if this.opacity > 0 and options.curtain_opacity > 0 then
			local ass = assdraw.ass_new()
			ass:new_event()
			ass:append('{\\blur0\\bord0\\1c&H'..options.color_background..'}')
			ass:append(ass_opacity(options.curtain_opacity, this.opacity))
			ass:pos(0, 0)
			ass:draw_start()
			ass:rect_cw(0, 0, display.width, display.height)
			ass:draw_stop()
			return ass
		end
	end
}))

-- CHAPTERS SERIALIZATION

-- Parse `chapter_ranges` option into workable data structure
for _, definition in ipairs(split(options.chapter_ranges, ' *,+ *')) do
	local start_patterns, color, opacity, end_patterns = string.match(definition, '([^<]+)<(%x%x%x%x%x%x):(%d?%.?%d*)>([^>]+)')

	-- Invalid definition
	if start_patterns == nil then goto continue end

	start_patterns = start_patterns:lower()
	end_patterns = end_patterns:lower()
	local uses_bof = start_patterns:find('{bof}') ~= nil
	local uses_eof = end_patterns:find('{eof}') ~= nil
	local chapter_range = {
		start_patterns = split(start_patterns, '|'),
		end_patterns = split(end_patterns, '|'),
		color = color,
		opacity = tonumber(opacity),
		ranges = {}
	}

	-- Filter out special keywords so we don't use them when matching titles
	if uses_bof then
		chapter_range.start_patterns = itable_remove(chapter_range.start_patterns, '{bof}')
	end
	if uses_eof and chapter_range.end_patterns then
		chapter_range.end_patterns = itable_remove(chapter_range.end_patterns, '{eof}')
	end

	chapter_range['serialize'] = function (chapters)
		chapter_range.ranges = {}
		local current_range = nil
		-- bof and eof should be used only once per timeline
		-- eof is only used when last range is missing end
		local bof_used = false

		function start_range(chapter)
			-- If there is already a range started, should we append or overwrite?
			-- I chose overwrite here.
			current_range = {['start'] = chapter}
		end

		function end_range(chapter)
			current_range['end'] = chapter
			chapter_range.ranges[#chapter_range.ranges + 1] = current_range
			-- Mark both chapter objects
			current_range['start']._uosc_used_as_range_point = true
			current_range['end']._uosc_used_as_range_point = true
			-- Clear for next range
			current_range = nil
		end

		for _, chapter in ipairs(chapters) do
			if type(chapter.title) == 'string' then
				local lowercase_title = chapter.title:lower()
				local is_end = false
				local is_start = false

				-- Is ending check and handling
				if chapter_range.end_patterns then
					for _, end_pattern in ipairs(chapter_range.end_patterns) do
						is_end = is_end or lowercase_title:find(end_pattern) ~= nil
					end

					if is_end then
						if current_range == nil and uses_bof and not bof_used then
							bof_used = true
							start_range({time = 0})
						end
						if current_range ~= nil then
							end_range(chapter)
						else
							is_end = false
						end
					end
				end

				-- Is start check and handling
				for _, start_pattern in ipairs(chapter_range.start_patterns) do
					is_start = is_start or lowercase_title:find(start_pattern) ~= nil
				end

				if is_start then start_range(chapter) end
			end
		end

		-- If there is an unfinished range and range type accepts eof, use it
		if current_range ~= nil and uses_eof then
			end_range({time = state.duration or infinity})
		end
	end

	state.chapter_ranges = state.chapter_ranges or {}
	state.chapter_ranges[#state.chapter_ranges + 1] = chapter_range

	::continue::
end

function parse_chapters()
	-- Sometimes state.duration is not initialized yet for some reason
	state.duration = mp.get_property_native('duration')

	local chapters = get_normalized_chapters()

	if not chapters or not state.duration then return end

	-- Reset custom ranges
	for _, chapter_range in ipairs(state.chapter_ranges or {}) do
		chapter_range.serialize(chapters)
	end

	-- Filter out chapters that were used as ranges
	state.chapters = itable_remove(chapters, function(chapter)
		return chapter._uosc_used_as_range_point == true
	end)

	request_render()
end

-- CONTEXT MENU SERIALIZATION

state.context_menu_items = (function()
	local input_conf_path = mp.command_native({'expand-path', '~~/input.conf'})
	local input_conf_meta, meta_error = utils.file_info(input_conf_path)

	-- File doesn't exist
	if not input_conf_meta or not input_conf_meta.is_file then return end

	local main_menu = {items = {}, items_by_command = {}}
	local submenus_by_id = {}

	for line in io.lines(input_conf_path) do
		local key, command, title = string.match(line, '%s*([%S]+)%s+(.*)%s#!%s*(.*)')
		if not key then
			key, command, title = string.match(line, '%s*([%S]+)%s+(.*)%s#menu:%s*(.*)')
		end
		if key then
			local is_dummy = key:sub(1, 1) == '#'
			local submenu_id = ''
			local target_menu = main_menu
			local title_parts = split(title or '', ' *> *')

			for index, title_part in ipairs(#title_parts > 0 and title_parts or {''}) do
				if index < #title_parts then
					submenu_id = submenu_id .. title_part

					if not submenus_by_id[submenu_id] then
						local items = {}
						submenus_by_id[submenu_id] = {items = items, items_by_command = {}}
						target_menu.items[#target_menu.items + 1] = {title = title_part, items = items}
					end

					target_menu = submenus_by_id[submenu_id]
				else
					-- If command is already in menu, just append the key to it
					if target_menu.items_by_command[command] then
						local hint = target_menu.items_by_command[command].hint
						target_menu.items_by_command[command].hint = hint and hint..', '..key or key
					else
						local item = {
							title = title_part,
							hint = not is_dummy and key or nil,
							value = command
						}
						target_menu.items_by_command[command] = item
						target_menu.items[#target_menu.items + 1] = item
					end
				end
			end
		end
	end

	if #main_menu.items > 0 then return main_menu.items end
end)()

-- EVENT HANDLERS

function create_state_setter(name)
	return function(_, value)
		state[name] = value
		elements:trigger('prop_'..name, value)
		request_render()
	end
end

function update_cursor_position()
	cursor.x, cursor.y = mp.get_mouse_pos()
	-- mpv reports initial mouse position on linux as (0, 0), which always
	-- displays the top bar, so we just swap this one coordinate to infinity
	if cursor.x == 0 and cursor.y == 0 then
		cursor.x = infinity
		cursor.y = infinity
	end
	update_proximities()
	request_render()
end

function handle_mouse_leave()
	-- Slowly fadeout elements that are currently visible
	for _, element_name in ipairs({'timeline', 'volume', 'top_bar'}) do
		local element = elements[element_name]
		if element and element.proximity > 0 then
			element:tween_property('forced_proximity', element:get_effective_proximity(), 0, function()
				element.forced_proximity = nil
			end)
		end
	end

	cursor.hidden = true
	update_proximities()
	elements:trigger('global_mouse_leave')
end

function handle_mouse_enter()
	cursor.hidden = false
	update_cursor_position()
	tween_element_stop(state)
	elements:trigger('global_mouse_enter')
end

function handle_mouse_move()
	-- Handle case when we are in cursor hidden state but not left the actual
	-- window (i.e. when autohide simulates mouse_leave).
	if cursor.hidden then
		handle_mouse_enter()
		return
	end

	update_cursor_position()
	elements:trigger('global_mouse_move')
	request_render()

	-- Restart timer that hides UI when mouse is autohidden
	if options.autohide then
		state.cursor_autohide_timer:kill()
		state.cursor_autohide_timer:resume()
	end
end

function navigate_directory(direction)
	local path = mp.get_property_native("path")

	if not path or is_protocol(path) then return end

	local next_file = get_adjacent_file(path, direction, options.media_types)

	if next_file then
		mp.commandv("loadfile", utils.join_path(serialize_path(path).dirname, next_file))
	end
end

function load_file_in_current_directory(index)
	local path = mp.get_property_native("path")

	if not path or is_protocol(path) then return end

	local dirname = serialize_path(path).dirname
	local files = get_files_in_directory(dirname, options.media_types)

	if not files then return end
	if index < 0 then index = #files + index + 1 end

	if files[index] then
		mp.commandv("loadfile", utils.join_path(dirname, files[index]))
	end
end

-- MENUS

function create_select_tracklist_type_menu_opener(menu_title, track_type, track_prop)
	return function()
		if menu:is_open(track_type) then menu:close() return end

		local items = {}
		local active_item = nil

		for index, track in ipairs(mp.get_property_native('track-list')) do
			if track.type == track_type then
				if track.selected then active_item = track.id end

				items[#items + 1] = {
					title = (track.title and track.title or 'Track '..track.id),
					hint = track.lang and track.lang:upper() or nil,
					value = track.id
				}
			end
		end

		-- Add option to disable a subtitle track. This works for all tracks,
		-- but why would anyone want to disable audio or video? Better to not
		-- let people mistakenly select what is unwanted 99.999% of the time.
		-- If I'm mistaken and there is an active need for this, feel free to
		-- open an issue.
		if track_type == 'sub' then
			active_item = active_item and active_item + 1 or 1
			table.insert(items, 1, {hint = 'disabled', value = nil})
		end

		menu:open(items, function(id)
			mp.commandv('set', track_prop, id and id or 'no')

			-- If subtitle track was selected, assume user also wants to see it
			if id and track_type == 'sub' then
				mp.commandv('set', 'sub-visibility', 'yes')
			end

			menu:close()
		end, {type = track_type, title = menu_title, active_item = active_item})
	end
end

-- `menu_options`:
-- **allowed_types** - table with file extensions to display
-- **active_path** - full path of a file to preselect
-- Rest of the options are passed to `menu:open()`
function open_file_navigation_menu(directory, handle_select, menu_options)
	directory = serialize_path(directory)
	local directories, error = utils.readdir(directory.path, 'dirs')
	local files, error = get_files_in_directory(directory.path, menu_options.allowed_types)
	local is_root = not directory.dirname

	if not files or not directories then
		msg.error('Retrieving files from '..directory..' failed: '..(error or ''))
		return
	end

	-- Files are already sorted
	table.sort(directories, word_order_comparator)

	-- Pre-populate items with parent directory selector if not at root
	local items = is_root and {} or {
		{title = '..', hint = 'parent dir', value = directory.dirname}
	}

	for _, dir in ipairs(directories) do
		local serialized = serialize_path(utils.join_path(directory.path, dir))
		items[#items + 1] = {title = serialized.basename, value = serialized.path, hint = '/'}
	end

	menu_options.active_item = nil

	for _, file in ipairs(files) do
		local serialized = serialize_path(utils.join_path(directory.path, file))
		local item_index = #items + 1

		items[item_index] = {
			title = serialized.basename,
			value = serialized.path,
		}

		if menu_options.active_path == serialized.path then
			menu_options.active_item = item_index
		end
	end

	menu_options.selected_item = menu_options.active_item or ((is_root == false and #files > 1) and 2 or 1)
	menu_options.title = directory.basename..'/'

	menu:open(items, function(path)
		local meta, error = utils.file_info(path)

		if not meta then
			msg.error('Retrieving file info for '..path..' failed: '..(error or ''))
			return
		end

		if meta.is_dir then
			open_file_navigation_menu(path, handle_select, menu_options)
		else
			handle_select(path)
			menu:close()
		end
	end, menu_options)
end

-- VALUE SERIALIZATION/NORMALIZATION

options.proximity_out = math.max(options.proximity_out, options.proximity_in + 1)
options.chapters = itable_find({'dots', 'lines', 'lines-top', 'lines-bottom'}, options.chapters) and options.chapters or 'none'
options.media_types = split(options.media_types, ' *, *')
options.subtitle_types = split(options.subtitle_types, ' *, *')
options.stream_quality_options = split(options.stream_quality_options, ' *, *')
options.timeline_cached_ranges = (function()
	if options.timeline_cached_ranges == '' or options.timeline_cached_ranges == 'no' then return nil end
	local parts = split(options.timeline_cached_ranges, ':')
	return parts[1] and {color = parts[1], opacity = tonumber(parts[2])} or nil
end)()
for _, name in ipairs({'timeline', 'volume', 'top_bar', 'speed'}) do
	local option_name = name..'_persistency'
	local flags = {}
	for _, state in ipairs(split(options[option_name], ' *, *')) do
		flags[state] = true
	end
	options[option_name] = flags
end

-- HOOKS
mp.register_event('file-loaded', parse_chapters)
mp.observe_property('track-list', 'native', function(name, value)
	-- checks if the file is audio only (mp3, etc)
	local has_audio = false
	local has_video = false
	for _, track in ipairs(value) do
		if track.type == 'audio' then has_audio = true end
		if track.type == 'video' and not track.albumart then has_video = true end
	end
	state.is_audio = not has_video and has_audio
end)
mp.observe_property('chapter-list', 'native', parse_chapters)
mp.observe_property('border', 'bool', create_state_setter('border'))
mp.observe_property('ab-loop-a', 'number', create_state_setter('ab_loop_a'))
mp.observe_property('ab-loop-b', 'number', create_state_setter('ab_loop_b'))
mp.observe_property('duration', 'number', create_state_setter('duration'))
mp.observe_property('media-title', 'string', create_state_setter('media_title'))
mp.observe_property('fullscreen', 'bool', function(_, value)
	state.fullscreen = value
	state.fullormaxed = state.fullscreen or state.maximized
	update_display_dimensions()
	elements:trigger('prop_fullscreen', value)
	elements:trigger('prop_fullormaxed', state.fullormaxed)
end)
mp.observe_property('window-maximized', 'bool', function(_, value)
	state.maximized = value
	state.fullormaxed = state.fullscreen or state.maximized
	update_display_dimensions()
	elements:trigger('prop_maximized', value)
	elements:trigger('prop_fullormaxed', state.fullormaxed)
end)
mp.observe_property('idle-active', 'bool', create_state_setter('idle'))
mp.observe_property('speed', 'number', create_state_setter('speed'))
mp.observe_property('pause', 'bool', create_state_setter('pause'))
mp.observe_property('volume', 'number', create_state_setter('volume'))
mp.observe_property('volume-max', 'number', create_state_setter('volume_max'))
mp.observe_property('mute', 'bool', create_state_setter('mute'))
mp.observe_property('playback-time', 'number', function(name, val)
	-- Ignore the initial call with nil value
	if val == nil then return end

	state.position = val
	state.elapsed_seconds = val
	state.elapsed_time = state.elapsed_seconds and mp.format_time(state.elapsed_seconds) or nil
	state.remaining_seconds = mp.get_property_native('playtime-remaining')
	state.remaining_time = state.remaining_seconds and mp.format_time(state.remaining_seconds) or nil

	request_render()
end)
mp.observe_property('osd-dimensions', 'native', function(name, val)
	update_display_dimensions()
	request_render()
end)
mp.observe_property('demuxer-cache-state', 'native', function(prop, cache_state)
	if cache_state == nil then
		state.cached_ranges = nil
		return
	end
	local cache_ranges = cache_state['seekable-ranges']
	state.cached_ranges = #cache_ranges > 0 and cache_ranges or nil
end)

-- CONTROLS

-- Mouse movement key binds
local base_keybinds = {
	{'mouse_move', handle_mouse_move},
	{'mouse_leave', handle_mouse_leave},
	{'mouse_enter', handle_mouse_enter},
}
if options.pause_on_click_shorter_than > 0 then
	-- Cycles pause when click is shorter than `options.pause_on_click_shorter_than`
	-- while filtering out double clicks.
	local duration_seconds = options.pause_on_click_shorter_than / 1000
	local last_down_event;
	local click_timer = mp.add_timeout(duration_seconds, function()
		mp.command('cycle pause')
	end);
	click_timer:kill()
	base_keybinds[#base_keybinds + 1] = {'mbtn_left', function()
			if mp.get_time() - last_down_event < duration_seconds then
				click_timer:resume()
			end
		end, function()
			if click_timer:is_enabled() then
				click_timer:kill()
				last_down_event = 0
			else
				last_down_event = mp.get_time()
			end
		end
	}
end
mp.set_key_bindings(base_keybinds, 'mouse_movement', 'force')
mp.enable_key_bindings('mouse_movement', 'allow-vo-dragging+allow-hide-cursor')

-- Context based key bind groups

forced_key_bindings = (function()
	function create_mouse_event_dispatcher(name)
		return function(...)
			for _, element in pairs(elements) do
				if element.proximity_raw == 0 then
					element:trigger(name, ...)
				end
				element:trigger('global_'..name, ...)
			end
		end
	end

	mp.set_key_bindings({
		{'mbtn_left', create_mouse_event_dispatcher('mbtn_left_up'), create_mouse_event_dispatcher('mbtn_left_down')},
		{'mbtn_left_dbl', 'ignore'},
	}, 'mbtn_left', 'force')
	mp.set_key_bindings({
		{'wheel_up', create_mouse_event_dispatcher('wheel_up')},
		{'wheel_down', create_mouse_event_dispatcher('wheel_down')},
	}, 'wheel', 'force')

	local groups = {}
	for _, group in ipairs({'mbtn_left', 'wheel'}) do
		groups[group] = {
			is_enabled = false,
			enable = function(this)
				if this.is_enabled then return end
				this.is_enabled = true
				mp.enable_key_bindings(group)
			end,
			disable = function(this)
				if not this.is_enabled then return end
				this.is_enabled = false
				mp.disable_key_bindings(group)
			end,
		}
	end
	return groups
end)()

-- KEY BINDABLE FEATURES

mp.add_key_binding(nil, 'peek-timeline', function()
	if elements.timeline.proximity > 0.5 then
		elements.timeline:tween_property('proximity', elements.timeline.proximity, 0)
	else
		elements.timeline:tween_property('proximity', elements.timeline.proximity, 1)
	end
end)
mp.add_key_binding(nil, 'toggle-progress', function()
	local timeline = elements.timeline
	if timeline.size_min_override then
		timeline:tween_property('size_min_override', timeline.size_min_override, timeline.size_min, function()
			timeline.size_min_override = nil
		end)
	else
		timeline:tween_property('size_min_override', timeline.size_min, 0)
	end
end)
mp.add_key_binding(nil, 'flash-timeline', function()
	elements.timeline:flash()
end)
mp.add_key_binding(nil, 'flash-top-bar', function()
	elements.top_bar:flash()
end)
mp.add_key_binding(nil, 'flash-volume', function()
	if elements.volume then elements.volume:flash() end
end)
mp.add_key_binding(nil, 'flash-speed', function()
	if elements.speed then elements.speed:flash() end
end)
mp.add_key_binding(nil, 'flash-pause-indicator', function()
	elements.pause_indicator:flash()
end)
mp.add_key_binding(nil, 'decide-pause-indicator', function()
	elements.pause_indicator:decide()
end)
function menu_key_binding()
  if menu:is_open('menu') then
    menu:close()
  elseif state.context_menu_items then
    menu:open(state.context_menu_items, function(command)
      mp.command(command)
    end, {type = 'menu'})
  end
end
mp.add_key_binding(nil, 'menu', menu_key_binding)
mp.add_key_binding(nil, 'load-subtitles', function()
	if menu:is_open('load-subtitles') then menu:close() return end

	local path = mp.get_property_native('path')
	if path and is_protocol(path) then
		path='$HOME'
	end
	open_file_navigation_menu(
		serialize_path(path).dirname,
		function(path) mp.commandv('sub-add', path) end,
		{
			type = 'load-subtitles',
			allowed_types = options.subtitle_types
		}
	)
end)
mp.add_key_binding(nil, 'subtitles', create_select_tracklist_type_menu_opener('Subtitles', 'sub', 'sid'))
mp.add_key_binding(nil, 'audio', create_select_tracklist_type_menu_opener('Audio', 'audio', 'aid'))
mp.add_key_binding(nil, 'video', create_select_tracklist_type_menu_opener('Video', 'video', 'vid'))
mp.add_key_binding(nil, 'playlist', function()
	if menu:is_open('playlist') then menu:close() return end

	function serialize_playlist()
		local pos = mp.get_property_number('playlist-pos-1', 0)
		local items = {}
		local active_item
		for index, item in ipairs(mp.get_property_native('playlist')) do
			local is_url = item.filename:find('://')
			items[index] = {
				title = is_url and item.filename or serialize_path(item.filename).basename,
				hint = tostring(index),
				value = index
			}

			if index == pos then active_item = index end
		end
		return items, active_item
	end

	-- Update active index and playlist content on playlist changes
	function handle_playlist_change()
		if menu:is_open('playlist') then
			local items, active_item = serialize_playlist()
			elements.menu:update({
				items = items,
				active_item = active_item
			})
		end
	end

	-- Items and active_item are set in the handle_playlist_change callback, since adding
	-- a property observer triggers its handler immediately, we just let that initialize the items.
	menu:open({}, function(index)
		mp.commandv('set', 'playlist-pos-1', tostring(index))
	end, {
		type = 'playlist',
		title = 'Playlist',
		on_open = function()
			mp.observe_property('playlist', 'native', handle_playlist_change)
			mp.observe_property('playlist-pos-1', 'native', handle_playlist_change)
		end,
		on_close = function()
			mp.unobserve_property(handle_playlist_change)
		end,
	})
end)
mp.add_key_binding(nil, 'chapters', function()
	if menu:is_open('chapters') then menu:close() return end

	local items = {}
	local chapters = get_normalized_chapters()

	for index, chapter in ipairs(chapters) do
		items[#items + 1] = {
			title = chapter.title or '',
			hint = mp.format_time(chapter.time),
			value = chapter.time
		}
	end

	-- Select first chapter from the end with time lower
	-- than current playing position (with 100ms leeway).
	function get_selected_chapter_index()
		local position = mp.get_property_native('playback-time')
		if not position then return nil end
		for index = #items, 1, -1 do
			if position - 0.1 > items[index].value then return index end
		end
	end

	-- Update selected chapter in chapter navigation menu
	function seek_handler()
		if menu:is_open('chapters') then
			elements.menu:activate_index(get_selected_chapter_index())
		end
	end

	menu:open(items, function(time)
		mp.commandv('seek', tostring(time), 'absolute')
	end, {
		type = 'chapters',
		title = 'Chapters',
		active_item = get_selected_chapter_index(),
		on_open = function() mp.register_event('seek', seek_handler) end,
		on_close = function() mp.unregister_event(seek_handler) end
	})
end)
mp.add_key_binding(nil, 'show-in-directory', function()
	local path = mp.get_property_native('path')

	-- Ignore URLs
	if not path or is_protocol(path) then return end

	path = normalize_path(path)

	if state.os == 'windows' then
		utils.subprocess_detached({args = {'explorer', '/select,', path}, cancellable = false})
	elseif state.os == 'macos' then
		utils.subprocess_detached({args = {'open', '-R', path}, cancellable = false})
	elseif state.os == 'linux' then
		local result = utils.subprocess({args = {'nautilus', path}, cancellable = false})

		-- Fallback opens the folder with xdg-open instead
		if result.status ~= 0 then
			utils.subprocess({args = {'xdg-open', serialize_path(path).dirname}, cancellable = false})
		end
	end
end)
mp.add_key_binding(nil, 'stream-quality', function()
	if menu:is_open('stream-quality') then menu:close() return end

	local ytdl_format = mp.get_property_native('ytdl-format')
	local active_item = nil
	local formats = {}

	for index, height in ipairs(options.stream_quality_options) do
		local format = 'bestvideo[height<=?'..height..']+bestaudio/best[height<=?'..height..']'
		formats[#formats + 1] = {
			title = height..'p',
			value = format
		}
		if format == ytdl_format then active_item = index end
	end

	menu:open(formats, function(format)
		mp.set_property('ytdl-format', format)

		-- Reload the video to apply new format
		-- This is taken from https://github.com/jgreco/mpv-youtube-quality
		-- which is in turn taken from https://github.com/4e6/mpv-reload/
		-- Dunno if playlist_pos shenanigans below are necessary.
		local playlist_pos = mp.get_property_number('playlist-pos')
		local duration = mp.get_property_native('duration')
		local time_pos = mp.get_property('time-pos')

		mp.set_property_number('playlist-pos', playlist_pos)

		-- Tries to determine live stream vs. pre-recordered VOD. VOD has non-zero
		-- duration property. When reloading VOD, to keep the current time position
		-- we should provide offset from the start. Stream doesn't have fixed start.
		-- Decent choice would be to reload stream from it's current 'live' positon.
		-- That's the reason we don't pass the offset when reloading streams.
		if duration and duration > 0 then
			local function seeker()
				mp.commandv('seek', time_pos, 'absolute')
				mp.unregister_event(seeker)
			end
			mp.register_event('file-loaded', seeker)
		end
	end, {
		type = 'stream-quality',
		title = 'Stream quality',
		active_item = active_item,
	})
end)
mp.add_key_binding(nil, 'open-file', function()
	if menu:is_open('open-file') then menu:close() return end

	local path = mp.get_property_native('path')
	local directory
	local active_file

	if path == nil or is_protocol(path) then
		local path = serialize_path(mp.command_native({'expand-path', '~/'}))
		directory = path.path
		active_file = nil
	else
		local path = serialize_path(path)
		directory = path.dirname
		active_file = path.path
	end

	-- Update selected file in directory navigation menu
	function handle_file_loaded()
		if menu:is_open('open-file') then
			local path = normalize_path(mp.get_property_native('path'))
			elements.menu:activate_value(path)
			elements.menu:select_value(path)
		end
	end

	open_file_navigation_menu(
		directory,
		function(path) mp.commandv('loadfile', path) end,
		{
			type = 'open-file',
			allowed_types = options.media_types,
			active_path = active_file,
			on_open = function() mp.register_event('file-loaded', handle_file_loaded) end,
			on_close = function() mp.unregister_event(handle_file_loaded) end,
		}
	)
end)
mp.add_key_binding(nil, 'next', function()
	if mp.get_property_native('playlist-count') > 1 then
		mp.command('playlist-next')
	else
		navigate_directory('forward')
	end
end)
mp.add_key_binding(nil, 'prev', function()
	if mp.get_property_native('playlist-count') > 1 then
		mp.command('playlist-prev')
	else
		navigate_directory('backward')
	end
end)
mp.add_key_binding(nil, 'next-file', function() navigate_directory('forward') end)
mp.add_key_binding(nil, 'prev-file', function() navigate_directory('backward') end)
mp.add_key_binding(nil, 'first', function()
	if mp.get_property_native('playlist-count') > 1 then
		mp.commandv('set', 'playlist-pos-1', '1')
	else
		load_file_in_current_directory(1)
	end
end)
mp.add_key_binding(nil, 'last', function()
	local playlist_count = mp.get_property_native('playlist-count')
	if playlist_count > 1 then
		mp.commandv('set', 'playlist-pos-1', tostring(playlist_count))
	else
		load_file_in_current_directory(-1)
	end
end)
mp.add_key_binding(nil, 'first-file', function() load_file_in_current_directory(1) end)
mp.add_key_binding(nil, 'last-file', function() load_file_in_current_directory(-1) end)
mp.add_key_binding(nil, 'delete-file-next', function()
	local playlist_count = mp.get_property_native('playlist-count')

	local next_file = nil

	local path = mp.get_property_native('path')
	local is_local_file = path and not is_protocol(path)

	if is_local_file then
		path = normalize_path(path)

		if menu:is_open('open-file') then
			elements.menu:delete_value(path)
		end
	end

	if playlist_count > 1 then
		mp.commandv('playlist-remove', 'current')
	else
		if is_local_file then
			next_file = get_adjacent_file(path, 'forward', options.media_types)
		end

		if next_file then
			mp.commandv('loadfile', next_file)
		else
			mp.commandv('stop')
		end
	end

	if is_local_file then delete_file(path) end
end)
mp.add_key_binding(nil, 'delete-file-quit', function()
	local path = mp.get_property_native('path')
	mp.command('stop')
	if path and not is_protocol(path) then delete_file(normalize_path(path)) end
	mp.command('quit')
end)
mp.add_key_binding(nil, 'open-config-directory', function()
	local config = serialize_path(mp.command_native({'expand-path', '~~/mpv.conf'}))
	local args

	if state.os == 'windows' then
		args = {'explorer', '/select,', config.path}
	elseif state.os == 'macos' then
		args = {'open', '-R', config.path}
	elseif state.os == 'linux' then
		args = {'xdg-open', config.dirname}
	end

	utils.subprocess_detached({args = args, cancellable = false})
end)


================================================
FILE: nvim/.config/nvim/colors/idk.vim
================================================
" vi:syntax=vim
" 
" Modified version of Base16 Tomorrow Night to tailored to my liking
" Original by Chris Kempson
" Modified by Siddharth Dushantha


" GUI color definitions
let s:gui00        = "101213"
let g:base16_gui00 = "101213"
let s:gui01        = "101213"
let g:base16_gui01 = "282a2e"
let s:gui02        = "373b41"
let g:base16_gui02 = "373b41"
let s:gui03        = "969896"
let g:base16_gui03 = "969896"
let s:gui04        = "b4b7b4"
let g:base16_gui04 = "b4b7b4"
let s:gui05        = "c5c8c6"
let g:base16_gui05 = "c5c8c6"
let s:gui06        = "e0e0e0"
let g:base16_gui06 = "e0e0e0"
let s:gui07        = "ffffff"
let g:base16_gui07 = "ffffff"
let s:gui08        = "cc6666"
let g:base16_gui08 = "cc6666"
let s:gui09        = "de935f"
let g:base16_gui09 = "de935f"
let s:gui0A        = "f0c674"
let g:base16_gui0A = "f0c674"
let s:gui0B        = "b5bd68"
let g:base16_gui0B = "b5bd68"
let s:gui0C        = "8abeb7"
let g:base16_gui0C = "8abeb7"
let s:gui0D        = "81a2be"
let g:base16_gui0D = "81a2be"
let s:gui0E        = "b294bb"
let g:base16_gui0E = "b294bb"
let s:gui0F        = "a3685a"
let g:base16_gui0F = "a3685a"

" Terminal color definitions
let s:cterm00        = "00"
let g:base16_cterm00 = "00"
let s:cterm03        = "08"
let g:base16_cterm03 = "08"
let s:cterm05        = "07"
let g:base16_cterm05 = "07"
let s:cterm07        = "15"
let g:base16_cterm07 = "15"
let s:cterm08        = "01"
let g:base16_cterm08 = "01"
let s:cterm0A        = "03"
let g:base16_cterm0A = "03"
let s:cterm0B        = "02"
let g:base16_cterm0B = "02"
let s:cterm0C        = "06"
let g:base16_cterm0C = "06"
let s:cterm0D        = "04"
let g:base16_cterm0D = "04"
let s:cterm0E        = "05"
let g:base16_cterm0E = "05"
if exists("base16colorspace") && base16colorspace == "256"
  let s:cterm01        = "18"
  let g:base16_cterm01 = "18"
  let s:cterm02        = "19"
  let g:base16_cterm02 = "19"
  let s:cterm04        = "20"
  let g:base16_cterm04 = "20"
  let s:cterm06        = "21"
  let g:base16_cterm06 = "21"
  let s:cterm09        = "16"
  let g:base16_cterm09 = "16"
  let s:cterm0F        = "17"
  let g:base16_cterm0F = "17"
else
  let s:cterm01        = "10"
  let g:base16_cterm01 = "10"
  let s:cterm02        = "11"
  let g:base16_cterm02 = "11"
  let s:cterm04        = "12"
  let g:base16_cterm04 = "12"
  let s:cterm06        = "13"
  let g:base16_cterm06 = "13"
  let s:cterm09        = "09"
  let g:base16_cterm09 = "09"
  let s:cterm0F        = "14"
  let g:base16_cterm0F = "14"
endif

" Neovim terminal colours
if has("nvim")
  let g:terminal_color_0 =  "#1d1f21"
  let g:terminal_color_1 =  "#cc6666"
  let g:terminal_color_2 =  "#b5bd68"
  let g:terminal_color_3 =  "#f0c674"
  let g:terminal_color_4 =  "#81a2be"
  let g:terminal_color_5 =  "#b294bb"
  let g:terminal_color_6 =  "#8abeb7"
  let g:terminal_color_7 =  "#c5c8c6"
  let g:terminal_color_8 =  "#969896"
  let g:terminal_color_9 =  "#cc6666"
  let g:terminal_color_10 = "#b5bd68"
  let g:terminal_color_11 = "#f0c674"
  let g:terminal_color_12 = "#81a2be"
  let g:terminal_color_13 = "#b294bb"
  let g:terminal_color_14 = "#8abeb7"
  let g:terminal_color_15 = "#ffffff"
  let g:terminal_color_background = g:terminal_color_0
  let g:terminal_color_foreground = g:terminal_color_5
  if &background == "light"
    let g:terminal_color_background = "#000000"
    let g:terminal_color_foreground = g:terminal_color_2
  endif
elseif has("terminal")
  let g:terminal_ansi_colors = [
        \ "#1d1f21",
        \ "#cc6666",
        \ "#b5bd68",
        \ "#f0c674",
        \ "#81a2be",
        \ "#b294bb",
        \ "#8abeb7",
        \ "#c5c8c6",
        \ "#969896",
        \ "#cc6666",
        \ "#b5bd68",
        \ "#f0c674",
        \ "#81a2be",
        \ "#b294bb",
        \ "#8abeb7",
        \ "#ffffff",
        \ ]
endif

" Theme setup
hi clear
syntax reset
let g:colors_name = "base16-tomorrow-night"

" Highlighting function
" Optional variables are attributes and guisp
function! g:Base16hi(group, guifg, guibg, ctermfg, ctermbg, ...)
  let l:attr = get(a:, 1, "")
  let l:guisp = get(a:, 2, "")

  if a:guifg != ""
    exec "hi " . a:group . " guifg=#" . a:guifg
  endif
  if a:guibg != ""
    exec "hi " . a:group . " guibg=#" . a:guibg
  endif
  if a:ctermfg != ""
    exec "hi " . a:group . " ctermfg=" . a:ctermfg
  endif
  if a:ctermbg != ""
    exec "hi " . a:group . " ctermbg=" . a:ctermbg
  endif
  if l:attr != ""
    exec "hi " . a:group . " gui=" . l:attr . " cterm=" . l:attr
  endif
  if l:guisp != ""
    exec "hi " . a:group . " guisp=#" . l:guisp
  endif
endfunction


fun <sid>hi(group, guifg, guibg, ctermfg, ctermbg, attr, guisp)
  call g:Base16hi(a:group, a:guifg, a:guibg, a:ctermfg, a:ctermbg, a:attr, a:guisp)
endfun

" Vim editor colors
call <sid>hi("Normal",        s:gui05, s:gui00, s:cterm05, s:cterm00, "", "")
call <sid>hi("Bold",          "", "", "", "", "bold", "")
call <sid>hi("Debug",         s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("Directory",     s:gui0D, "", s:cterm0D, "", "", "")
call <sid>hi("Error",         s:gui00, s:gui08, s:cterm00, s:cterm08, "", "")
call <sid>hi("ErrorMsg",      s:gui08, s:gui00, s:cterm08, s:cterm00, "", "")
call <sid>hi("Exception",     s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("FoldColumn",    s:gui0C, s:gui01, s:cterm0C, s:cterm01, "", "")
call <sid>hi("Folded",        s:gui03, s:gui01, s:cterm03, s:cterm01, "", "")
call <sid>hi("IncSearch",     s:gui01, s:gui09, s:cterm01, s:cterm09, "none", "")
call <sid>hi("Italic",        "", "", "", "", "none", "")
call <sid>hi("Macro",         s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("MatchParen",    "", s:gui03, "", s:cterm03,  "", "")
call <sid>hi("ModeMsg",       s:gui0B, "", s:cterm0B, "", "", "")
call <sid>hi("MoreMsg",       s:gui0B, "", s:cterm0B, "", "", "")
call <sid>hi("Question",      s:gui0D, "", s:cterm0D, "", "", "")
call <sid>hi("Search",        s:gui01, s:gui0A, s:cterm01, s:cterm0A,  "", "")
call <sid>hi("Substitute",    s:gui01, s:gui0A, s:cterm01, s:cterm0A, "none", "")
call <sid>hi("SpecialKey",    s:gui03, "", s:cterm03, "", "", "")
call <sid>hi("TooLong",       s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("Underlined",    s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("Visual",        "", s:gui02, "", s:cterm02, "", "")
call <sid>hi("VisualNOS",     s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("WarningMsg",    s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("WildMenu",      s:gui08, s:gui0A, s:cterm08, "", "", "")
call <sid>hi("Title",         s:gui0D, "", s:cterm0D, "", "none", "")
call <sid>hi("Conceal",       s:gui0D, s:gui00, s:cterm0D, s:cterm00, "", "")
call <sid>hi("Cursor",        s:gui00, s:gui05, s:cterm00, s:cterm05, "", "")
call <sid>hi("NonText",       s:gui03, "", s:cterm03, "", "", "")
call <sid>hi("LineNr",        s:gui03, s:gui01, s:cterm03, s:cterm01, "", "")
call <sid>hi("SignColumn",    s:gui03, s:gui01, s:cterm03, s:cterm01, "", "")
call <sid>hi("StatusLine",    s:gui04, s:gui02, s:cterm04, s:cterm02, "none", "")
call <sid>hi("StatusLineNC",  s:gui03, s:gui01, s:cterm03, s:cterm01, "none", "")
call <sid>hi("VertSplit",     s:gui02, s:gui02, s:cterm02, s:cterm02, "none", "")
call <sid>hi("ColorColumn",   "", s:gui01, "", s:cterm01, "none", "")
call <sid>hi("CursorColumn",  "", s:gui01, "", s:cterm01, "none", "")
call <sid>hi("CursorLine",    "", s:gui01, "", s:cterm01, "none", "")
call <sid>hi("CursorLineNr",  s:gui04, s:gui01, s:cterm04, s:cterm01, "", "")
call <sid>hi("QuickFixLine",  "", s:gui01, "", s:cterm01, "none", "")
call <sid>hi("PMenu",         s:gui05, s:gui01, s:cterm05, s:cterm01, "none", "")
call <sid>hi("PMenuSel",      s:gui01, s:gui05, s:cterm01, s:cterm05, "", "")
call <sid>hi("TabLine",       s:gui03, s:gui01, s:cterm03, s:cterm01, "none", "")
call <sid>hi("TabLineFill",   s:gui03, s:gui01, s:cterm03, s:cterm01, "none", "")
call <sid>hi("TabLineSel",    s:gui0B, s:gui01, s:cterm0B, s:cterm01, "none", "")

" Standard syntax highlighting
call <sid>hi("Boolean",      s:gui09, "", s:cterm09, "", "", "")
call <sid>hi("Character",    s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("Comment",      s:gui03, "", s:cterm03, "", "", "")
call <sid>hi("Conditional",  s:gui0E, "", s:cterm0E, "", "", "")
call <sid>hi("Constant",     s:gui09, "", s:cterm09, "", "", "")
call <sid>hi("Define",       s:gui0E, "", s:cterm0E, "", "none", "")
call <sid>hi("Delimiter",    s:gui0F, "", s:cterm0F, "", "", "")
call <sid>hi("Float",        s:gui09, "", s:cterm09, "", "", "")
call <sid>hi("Function",     s:gui0D, "", s:cterm0D, "", "", "")
call <sid>hi("Identifier",   s:gui08, "", s:cterm08, "", "none", "")
call <sid>hi("Include",      s:gui0D, "", s:cterm0D, "", "", "")
call <sid>hi("Keyword",      s:gui0E, "", s:cterm0E, "", "", "")
call <sid>hi("Label",        s:gui0A, "", s:cterm0A, "", "", "")
call <sid>hi("Number",       s:gui09, "", s:cterm09, "", "", "")
call <sid>hi("Operator",     s:gui05, "", s:cterm05, "", "none", "")
call <sid>hi("PreProc",      s:gui0A, "", s:cterm0A, "", "", "")
call <sid>hi("Repeat",       s:gui0A, "", s:cterm0A, "", "", "")
call <sid>hi("Special",      s:gui0C, "", s:cterm0C, "", "", "")
call <sid>hi("SpecialChar",  s:gui0F, "", s:cterm0F, "", "", "")
call <sid>hi("Statement",    s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("StorageClass", s:gui0A, "", s:cterm0A, "", "", "")
call <sid>hi("String",       s:gui0B, "", s:cterm0B, "", "", "")
call <sid>hi("Structure",    s:gui0E, "", s:cterm0E, "", "", "")
call <sid>hi("Tag",          s:gui0A, "", s:cterm0A, "", "", "")
call <sid>hi("Todo",         s:gui0A, s:gui01, s:cterm0A, s:cterm01, "", "")
call <sid>hi("Type",         s:gui0A, "", s:cterm0A, "", "none", "")
call <sid>hi("Typedef",      s:gui0A, "", s:cterm0A, "", "", "")

" C highlighting
call <sid>hi("cOperator",   s:gui0C, "", s:cterm0C, "", "", "")
call <sid>hi("cPreCondit",  s:gui0E, "", s:cterm0E, "", "", "")

" C# highlighting
call <sid>hi("csClass",                 s:gui0A, "", s:cterm0A, "", "", "")
call <sid>hi("csAttribute",             s:gui0A, "", s:cterm0A, "", "", "")
call <sid>hi("csModifier",              s:gui0E, "", s:cterm0E, "", "", "")
call <sid>hi("csType",                  s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("csUnspecifiedStatement",  s:gui0D, "", s:cterm0D, "", "", "")
call <sid>hi("csContextualStatement",   s:gui0E, "", s:cterm0E, "", "", "")
call <sid>hi("csNewDecleration",        s:gui08, "", s:cterm08, "", "", "")

" CSS highlighting
call <sid>hi("cssBraces",      s:gui05, "", s:cterm05, "", "", "")
call <sid>hi("cssClassName",   s:gui0E, "", s:cterm0E, "", "", "")
call <sid>hi("cssColor",       s:gui0C, "", s:cterm0C, "", "", "")

" Diff highlighting
call <sid>hi("DiffAdd",      s:gui0B, s:gui01,  s:cterm0B, s:cterm01, "", "")
call <sid>hi("DiffChange",   s:gui03, s:gui01,  s:cterm03, s:cterm01, "", "")
call <sid>hi("DiffDelete",   s:gui08, s:gui01,  s:cterm08, s:cterm01, "", "")
call <sid>hi("DiffText",     s:gui0D, s:gui01,  s:cterm0D, s:cterm01, "", "")
call <sid>hi("DiffAdded",    s:gui0B, s:gui00,  s:cterm0B, s:cterm00, "", "")
call <sid>hi("DiffFile",     s:gui08, s:gui00,  s:cterm08, s:cterm00, "", "")
call <sid>hi("DiffNewFile",  s:gui0B, s:gui00,  s:cterm0B, s:cterm00, "", "")
call <sid>hi("DiffLine",     s:gui0D, s:gui00,  s:cterm0D, s:cterm00, "", "")
call <sid>hi("DiffRemoved",  s:gui08, s:gui00,  s:cterm08, s:cterm00, "", "")

" Git highlighting
call <sid>hi("gitcommitOverflow",       s:gui08, "", s:cterm08, "", "", "")
call <sid>hi("gitcommitSummary",        s:gui0B, "", s:cterm0B, "", "", "")
call <sid>hi("gitcommitComment",        s:gui03, "", s:cterm03, "", "", "")
call <sid>hi("gitcommitUntracked",      s:gui03, "", s:cterm03, "", "", "")
call <sid>hi("gitcommitDiscarded",      s:gui03, "", s:cterm03, "", "", "")
call <sid>hi("gitcommitSelected",       s:gui03, "", s:cterm03, "", "", "")
call <sid>hi("gitcommitHeader",         s:gui0E, "", s:cterm0E, "", "", "")
call <sid>hi("gitcommitSelectedType",   s:gui0D, "", s:cterm0D, "", "", "")
call <sid>hi("gitcommitUnmergedType",   s:gui0D, "", s:cterm0D, "", "", "")
call <sid>hi("gitcommitDiscardedType",  s:gui0D, "", s:
Download .txt
gitextract_p0ok6nnr/

├── .gitignore
├── X11/
│   ├── .Xresources
│   └── .xinitrc
├── alacritty/
│   └── .config/
│       └── alacritty/
│           └── alacritty.toml
├── bin/
│   └── bin/
│       ├── applications/
│       │   └── radio
│       ├── bugbounty/
│       │   ├── deadlinks
│       │   └── vdp
│       ├── just4fun/
│       │   ├── 10print
│       │   ├── 2048
│       │   ├── bee
│       │   ├── groot
│       │   └── panes
│       ├── keybinded/
│       │   ├── brightness/
│       │   │   ├── brightness
│       │   │   ├── brightnessControl.sh
│       │   │   └── restoreBrightness.sh
│       │   ├── music_ctrl.sh
│       │   ├── pop_mpv.sh
│       │   ├── rofi_notes.sh
│       │   └── vifm.py
│       ├── light-theme/
│       │   └── libreoffice.sh
│       └── utils/
│           ├── 0x0
│           ├── add-shadow
│           ├── aperisolve
│           ├── border
│           ├── ce
│           ├── cnf
│           ├── darkmode.sh
│           ├── duckmail
│           ├── ew
│           ├── ex
│           ├── ffmpeg-wrappers/
│           │   ├── vid2
│           │   ├── vidcut
│           │   └── vidmute
│           ├── fwifi
│           ├── gifgen
│           ├── gym
│           ├── h2s
│           ├── kp
│           ├── mmv
│           ├── notes
│           ├── ocr
│           ├── pauseallmpv
│           ├── qrshot
│           ├── rofi-askpass
│           ├── sk
│           ├── sloc
│           ├── tmpjn
│           ├── tmpsh
│           ├── touchpad
│           ├── upld
│           ├── urldecode
│           ├── urlencode
│           ├── webcam
│           └── xcwd-helper
├── discord/
│   └── .config/
│       └── discord/
│           └── settings.json
├── dunst/
│   └── .config/
│       └── dunst/
│           └── dunstrc
├── flameshot/
│   └── .config/
│       └── flameshot/
│           └── flameshot.ini
├── gtk-2.0/
│   └── .config/
│       └── gtk-2.0/
│           └── gtkfilechooser.ini
├── gtk-3.0/
│   └── .config/
│       └── gtk-3.0/
│           ├── bookmarks
│           └── settings.ini
├── i3/
│   └── .config/
│       └── i3/
│           └── config
├── mimetype/
│   ├── .config/
│   │   └── mimeapps.list
│   └── .local/
│       └── share/
│           └── applications/
│               ├── browser.desktop
│               ├── img.desktop
│               ├── pdf.desktop
│               ├── text.desktop
│               └── video.desktop
├── mpv/
│   └── .config/
│       └── mpv/
│           ├── input.conf
│           ├── mpv.conf
│           └── scripts/
│               └── uosc.lua
├── nvim/
│   └── .config/
│       └── nvim/
│           ├── colors/
│           │   ├── idk.vim
│           │   └── test.vim
│           ├── init.lua
│           └── lua/
│               ├── mappings.lua
│               ├── options.lua
│               └── plugins/
│                   ├── configs/
│                   │   ├── bufferline.lua
│                   │   └── lualine.lua
│                   └── init.lua
├── other/
│   └── .config/
│       └── user-dirs.dirs
├── picom/
│   └── .config/
│       └── picom/
│           └── picom.conf
├── polybar/
│   └── .config/
│       └── polybar/
│           ├── config.ini
│           ├── launch.sh
│           └── scripts/
│               ├── battery_widget.sh
│               ├── bluetooth.sh
│               ├── mic_status.sh
│               ├── today.sh
│               ├── vpn-ip.sh
│               └── wifi_widget.sh
├── rofi/
│   └── .config/
│       └── rofi/
│           ├── config.rasi
│           ├── scripts/
│           │   ├── chars.txt
│           │   ├── rofi-farge.sh
│           │   ├── rofi-finder.sh
│           │   └── rofi-picker.sh
│           └── themes/
│               ├── askpass.rasi
│               ├── default.rasi
│               └── run.rasi
├── vifm/
│   └── .config/
│       └── vifm/
│           ├── colors/
│           │   ├── Default.vifm
│           │   └── minimal.vifm
│           ├── scripts/
│           │   └── README
│           ├── vifm-help.txt
│           └── vifmrc
├── wget/
│   └── .config/
│       └── wgetrc
└── zsh/
    ├── .config/
    │   └── aliases
    ├── .zprofile
    ├── .zshenv
    └── .zshrc
Download .txt
SYMBOL INDEX (1 symbols across 1 files)

FILE: bin/bin/keybinded/vifm.py
  function on (line 10) | def on(i3, e):
Condensed preview — 106 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (846K chars).
[
  {
    "path": ".gitignore",
    "chars": 241,
    "preview": "mpd/.config/mpd/mpd.log\nmpd/.config/mpd/mpdstate\nmpd/.config/mpd/mpd.pid\nmpd/.config/mpd/mpd.db\n\nnvim/.config/nvim/.netr"
  },
  {
    "path": "X11/.Xresources",
    "chars": 208,
    "preview": "Xft.dpi: 120\n\n! These might also be useful depending on your monitor and personal preference:\nXft.autohint: 0\nXft.lcdfil"
  },
  {
    "path": "X11/.xinitrc",
    "chars": 739,
    "preview": "#!/bin/sh\n#\n# ~/.xinitrc\n#\n# Executed by startx (run your window manager from here)\n# \n# NOTICE: the exec commands MUST "
  },
  {
    "path": "alacritty/.config/alacritty/alacritty.toml",
    "chars": 322,
    "preview": "[font]\nsize = 10.4\n\n[font.bold]\nfamily = \"JetBrainsMono NerdFont\"\nstyle = \"Bold\"\n\n[font.bold_italic]\nfamily = \"JetBrains"
  },
  {
    "path": "bin/bin/applications/radio",
    "chars": 5584,
    "preview": "#!/usr/bin/env bash\n#\n# Siddharth Dushantha 2020\n#\n\nversion=\"1.0.0\"\nconfig_file=\"$HOME/.config/radio/config.json\"\ncache_"
  },
  {
    "path": "bin/bin/bugbounty/deadlinks",
    "chars": 1910,
    "preview": "#!/usr/bin/env sh\n#\n# by Siddharth Dushantha 2021\n#\n# A wrapper around blc[1] and subfinder[2] which finds dead links th"
  },
  {
    "path": "bin/bin/bugbounty/vdp",
    "chars": 2424,
    "preview": "#!/usr/bin/env bash\n#\n# by Siddharth Dushantha 2022\n#\n\nusage(){\n    cat <<EOF\nvdp [OPTIONS]\n\nOPTIONS\n-d, --domain  Domai"
  },
  {
    "path": "bin/bin/just4fun/10print",
    "chars": 157,
    "preview": "#!/usr/bin/env python3\n\n# Creates the famous 10print art, nothing special\nimport random\nfor i in range(100000):print(chr"
  },
  {
    "path": "bin/bin/just4fun/bee",
    "chars": 256,
    "preview": "#!/usr/bin/env python\n\nBEE = \"\"\"\\n\\033[1m     \\033[32m\"Bee\" careful    \\033[34m__\n       \\033[32mwith sudo!    \\033[34m/"
  },
  {
    "path": "bin/bin/just4fun/groot",
    "chars": 380,
    "preview": "     \u001b[00;32m  \\^V//\n     \u001b[00;33m  |\u001b[01;37m. \u001b[01;37m.\u001b[00;33m|   \u001b[01;34m I AM (G)ROOT!\n     \u001b[00;32m- \u001b[00;33m\\ - / "
  },
  {
    "path": "bin/bin/just4fun/panes",
    "chars": 603,
    "preview": "#!/usr/bin/env bash\n\n# Author: GekkoP\n# Source: http://linuxbbq.org/bbs/viewtopic.php?f=4&t=1656#p33189\n \nf=3 b=4\nfor j "
  },
  {
    "path": "bin/bin/keybinded/brightness/brightness",
    "chars": 10,
    "preview": "33.453367\n"
  },
  {
    "path": "bin/bin/keybinded/brightness/brightnessControl.sh",
    "chars": 1254,
    "preview": "#!/usr/bin/env bash\n\n# You can call this script like this:\n# $ ./brightnessControl.sh up\n# $ ./brightnessControl.sh down"
  },
  {
    "path": "bin/bin/keybinded/brightness/restoreBrightness.sh",
    "chars": 171,
    "preview": "#!/usr/bin/env bash\n#\n# Restore the brightness by taking the value in the file, brightness\n\nVALUE=$(cat $HOME/bin/keybin"
  },
  {
    "path": "bin/bin/keybinded/music_ctrl.sh",
    "chars": 253,
    "preview": "#!/usr/bin/env bash\n#\n# mpDris2  is needed\n_command=\"$1\"\n\nif [ \"$1\" == \"toggle\" ]; then\n    if [ $(playerctl status) == "
  },
  {
    "path": "bin/bin/keybinded/pop_mpv.sh",
    "chars": 1602,
    "preview": "#!/usr/bin/env bash\n#\n# Created by Siddharth Dushantha (sdushantha)\n#\n# Dependencies: xdotool, mpv, xclip, youtube-dl\n# "
  },
  {
    "path": "bin/bin/keybinded/rofi_notes.sh",
    "chars": 268,
    "preview": "#!/usr/bin/env bash\n#\n# Use rofi to select/create notes and then edit them using nvim\n#\n\nnotes_directory=\"$HOME/document"
  },
  {
    "path": "bin/bin/keybinded/vifm.py",
    "chars": 397,
    "preview": "#!/usr/bin/env python\n\nimport sys\nimport subprocess\nimport i3ipc\nimport os\n\ni3 = i3ipc.Connection()\n\ndef on(i3, e):\n    "
  },
  {
    "path": "bin/bin/light-theme/libreoffice.sh",
    "chars": 530,
    "preview": "#!/usr/bin/env bash\n#\n# This script allows me to run libreoffice with a light GTK theme.\n# To be able to get the light t"
  },
  {
    "path": "bin/bin/utils/0x0",
    "chars": 263,
    "preview": "#!/usr/bin/env bash\nURL=\"https://0x0.st\"\n\nif [ $# -eq 0 ]; then\n    echo \"Usage: 0x0.st FILE\\n\"\n    exit 1\nfi\n\nFILE=$1\n\n"
  },
  {
    "path": "bin/bin/utils/add-shadow",
    "chars": 378,
    "preview": "#!/usr/bin/env bash\n\n# This script adds a cool shadow effect to images, just like MacOS screenshots.\n# I usually use thi"
  },
  {
    "path": "bin/bin/utils/aperisolve",
    "chars": 344,
    "preview": "#!/usr/bin/env bash\nHOST=\"https://www.aperisolve.com\"\nARGC=$#\nEXPECTED_ARGS=1\n\nif [ $# -eq $EXPECTED_ARGS ]\nthen\n    P=$"
  },
  {
    "path": "bin/bin/utils/border",
    "chars": 370,
    "preview": "#!/usr/bin/env sh\n#\n# Siddharth Dushantha\n#\n# Turn the i3wm border on/off and change the size\n#\n\nset_border(){\n    i3-ms"
  },
  {
    "path": "bin/bin/utils/ce",
    "chars": 197,
    "preview": "#!/usr/bin/env bash\n#\n# This script lets me compile and execute in one go.\n# \n# Usage: ce CODE OUTPUT\n#\n# Example:\n#  ce"
  },
  {
    "path": "bin/bin/utils/cnf",
    "chars": 1820,
    "preview": "#!/usr/bin/env sh\n#\n# by Siddharth Dushantha 2021\n#\n# cnf - Command Not Found\n#\n# An utility which get the previous comm"
  },
  {
    "path": "bin/bin/utils/darkmode.sh",
    "chars": 747,
    "preview": "#!/usr/bin/env sh\n\n\nsetGTKTheme(){\n    # I run i3 along with GNOME services in the background, therefore\n    # I'm able "
  },
  {
    "path": "bin/bin/utils/duckmail",
    "chars": 3190,
    "preview": "#!/usr/bin/env sh\n#\n# by Siddharth Dushantha 2023\n#\n# Dependencies: jq, curl, xclip\n#\n# duckmail is a POSIX shell script"
  },
  {
    "path": "bin/bin/utils/ew",
    "chars": 361,
    "preview": "#!/bin/sh\n#\n# Siddharth Dushantha 2020\n#\n# https://github.com/sdushantha/bin\n#\n# ew - Edit Which\n# Quickly edit the sour"
  },
  {
    "path": "bin/bin/utils/ex",
    "chars": 1799,
    "preview": "#!/usr/bin/env bash\n#\n# A better way to extract archives.\n# I got this from the web, so credits goes to who ever wrote t"
  },
  {
    "path": "bin/bin/utils/ffmpeg-wrappers/vid2",
    "chars": 283,
    "preview": "#!/usr/bin/env bash\n#\n# Convert a video to...MP4, AVI, etc\n#\n# usage: vid2 FILE_FORMAT FILE\n#\n\nFILE_FORMAT=\"$1\"\nFILE=\"$2"
  },
  {
    "path": "bin/bin/utils/ffmpeg-wrappers/vidcut",
    "chars": 280,
    "preview": "#!/usr/bin/env bash\n#\n# Cut a video from timestamp x to y.\n#\n# Example:\n#   vid-cut myvideo.mp4 00:01 00:12 output.mp4\n#"
  },
  {
    "path": "bin/bin/utils/ffmpeg-wrappers/vidmute",
    "chars": 182,
    "preview": "#!/usr/bin/env bash\n#\n# Remove audio from a video file\n#\n# usage: vid-mute myvideo.mp4 myvideo-muted.mp4\n#\n\nINPUT=\"$1\"\nO"
  },
  {
    "path": "bin/bin/utils/fwifi",
    "chars": 623,
    "preview": "#!/usr/bin/env bash\n\n\nhas() {\n  local verbose=false\n  if [[ $1 == '-v' ]]; then\n    verbose=true\n    shift\n  fi\n  for c "
  },
  {
    "path": "bin/bin/utils/gifgen",
    "chars": 1827,
    "preview": "#!/usr/bin/env bash\n\n# Echo help/usage message\nshow_help() {\n  echo \"gifgen 1.1.2\"\n  echo\n  echo \"Usage: gifgen [options"
  },
  {
    "path": "bin/bin/utils/gym",
    "chars": 294,
    "preview": "#!/usr/bin/env python3\n#\n# Siddharth Dushantha 2022\n#\n# Check number of people at the gym \n#\n\nimport requests\nimport re\n"
  },
  {
    "path": "bin/bin/utils/h2s",
    "chars": 449,
    "preview": "#!/usr/bin/env sh\n#\n# by Siddharth Dushantha\n#\n# Change the HTTPS git url to a SSH git url\n#\n\nurl=$(git config --get rem"
  },
  {
    "path": "bin/bin/utils/kp",
    "chars": 477,
    "preview": "#!/usr/bin/env bash\n# mnemonic: [K]ill [P]rocess\n# show output of \"ps -ef\", use [tab] to select one or multiple entries\n"
  },
  {
    "path": "bin/bin/utils/mmv",
    "chars": 1159,
    "preview": "#!/usr/bin/env bash\nset -eu\n\n# Lists the current directory's files in Vim, so you can edit it and save to rename them\n# "
  },
  {
    "path": "bin/bin/utils/notes",
    "chars": 192,
    "preview": "#!/usr/bin/env sh\n\nnotes_dir=\"$HOME/documents/notes\"\nfile_name=$(ls \"$notes_dir\" | fzf)\n\nif [ -z \"$file_name\" ]; then\n  "
  },
  {
    "path": "bin/bin/utils/ocr",
    "chars": 2169,
    "preview": "#!/usr/bin/env bash\n#\n# Siddharth Dushantha 2020\n# \n# https://github.com/sdushantha/bin\n#\n\nTEXT_FILE=\"/tmp/ocr.txt\"\nIMAG"
  },
  {
    "path": "bin/bin/utils/pauseallmpv",
    "chars": 147,
    "preview": "#!/usr/bin/env bash\nfor i in /tmp/mpvsoc*; do\n    [ -e \"$i\" ] || break\n\techo '{ \"command\": [\"set_property\", \"pause\", tru"
  },
  {
    "path": "bin/bin/utils/qrshot",
    "chars": 1604,
    "preview": "#!/usr/bin/env bash\n#\n# Siddharth Dushantha 2022\n# \n# https://github.com/sdushantha/bin\n#\n\nimage_file=\"/tmp/ocr.png\"\n\n# "
  },
  {
    "path": "bin/bin/utils/rofi-askpass",
    "chars": 132,
    "preview": "#!/usr/bin/env bash\nrofi -dmenu\\\n    -password\\\n    -i\\\n    -no-fixed-num-lines\\\n    -p \"Password:\"\\\n    -theme themes/a"
  },
  {
    "path": "bin/bin/utils/sk",
    "chars": 427,
    "preview": "#!/usr/bin/env bash\n#\n# Toggle screenkey\n#\n\nif pgrep screenkey > /dev/null 2>&1; then\n    killall screenkey > /dev/null "
  },
  {
    "path": "bin/bin/utils/sloc",
    "chars": 525,
    "preview": "#!/bin/sh\n#\n# http://github.com/mitchweaver/bin\n#\n# count lines of code in a shellscript\n# ignores comments and blank li"
  },
  {
    "path": "bin/bin/utils/tmpjn",
    "chars": 468,
    "preview": "#!/usr/bin/env sh\n#\n# by Siddharth Dushantha 2020\n#\n# tmpjn - Temporary Jupyter Notebook\n#\n\nnb_file_name=\"notebook.ipynb"
  },
  {
    "path": "bin/bin/utils/tmpsh",
    "chars": 158,
    "preview": "#!/usr/bin/env bash\n#\n# http://github.com/mitchweaver\n#\n# open a shell in a temporary dir without adding commands to his"
  },
  {
    "path": "bin/bin/utils/touchpad",
    "chars": 604,
    "preview": "#!/usr/bin/env bash\n#\n# Siddharth Dushantha 2021\n#\n# Disable/enable the touchpad\n#\n\nposition=$(xinput list --name-only |"
  },
  {
    "path": "bin/bin/utils/upld",
    "chars": 746,
    "preview": "#!/usr/bin/env sh\nif [ $# -eq 0 ];then\n    echo -e \"No arguments specified.\\nUsage:\\n  transfer <file|directory>\\n  ... "
  },
  {
    "path": "bin/bin/utils/urldecode",
    "chars": 101,
    "preview": "#!/usr/bin/env python3\nimport sys\nimport urllib.parse\n\nprint(urllib.parse.unquote_plus(sys.argv[1]))\n"
  },
  {
    "path": "bin/bin/utils/urlencode",
    "chars": 92,
    "preview": "#!/usr/bin/env python3\nimport sys, urllib.parse\nprint(urllib.parse.quote_plus(sys.argv[1]))\n"
  },
  {
    "path": "bin/bin/utils/webcam",
    "chars": 253,
    "preview": "#!/usr/bin/env bash\n#\n# Show webcam\n#\n\nmpv --demuxer-lavf-format=video4linux2 \\\n    --demuxer-lavf-o-set=input_format=mj"
  },
  {
    "path": "bin/bin/utils/xcwd-helper",
    "chars": 752,
    "preview": "#!/usr/bin/env bash\n#\n# by Siddharth Dushantha 2023\n#\n# A script that only allows xcwd to be used for opening a terminal"
  },
  {
    "path": "discord/.config/discord/settings.json",
    "chars": 194,
    "preview": "{\n  \"chromiumSwitches\": {},\n  \"IS_MAXIMIZED\": false,\n  \"IS_MINIMIZED\": false,\n  \"WINDOW_BOUNDS\": {\n    \"x\": 0,\n    \"y\": "
  },
  {
    "path": "dunst/.config/dunst/dunstrc",
    "chars": 4115,
    "preview": "[global]\n   monitor = 0\n\n    # If this option is set to mouse or keyboard, the monitor option\n    # will be ignored.\n   "
  },
  {
    "path": "flameshot/.config/flameshot/flameshot.ini",
    "chars": 484,
    "preview": "[General]\ncheckForUpdates=false\ncontrastOpacity=102\ncontrastUiColor=#7c8fa3\ndisabledTrayIcon=true\ndrawColor=#ff0000\ndraw"
  },
  {
    "path": "gtk-2.0/.config/gtk-2.0/gtkfilechooser.ini",
    "chars": 201,
    "preview": "[Filechooser Settings]\nLocationMode=path-bar\nShowHidden=false\nShowSizeColumn=true\nGeometryX=1020\nGeometryY=0\nGeometryWid"
  },
  {
    "path": "gtk-3.0/.config/gtk-3.0/bookmarks",
    "chars": 204,
    "preview": "file:///home/siddharth/documents\nfile:///home/siddharth/downloads\nfile:///home/siddharth/pictures\nfile:///home/siddharth"
  },
  {
    "path": "gtk-3.0/.config/gtk-3.0/settings.ini",
    "chars": 408,
    "preview": "[Settings]\ngtk-theme-name=Arc-Dark\ngtk-icon-theme-name=Paper\ngtk-font-name=Noto Sans 11\ngtk-cursor-theme-name=Adwaita\ngt"
  },
  {
    "path": "i3/.config/i3/config",
    "chars": 8516,
    "preview": "# Norwegian speacial letters\n# Æ = ae\n# Ø = oslash\n# Å = aring\n\n# General {{{  \n# Define names for default workspaces fo"
  },
  {
    "path": "mimetype/.config/mimeapps.list",
    "chars": 431,
    "preview": "[Default Applications]\ninode/directory=thunar.desktop;\ntext/x-shellscript=text.desktop;\ntext/plain=text.desktop;\ntext/x-"
  },
  {
    "path": "mimetype/.local/share/applications/browser.desktop",
    "chars": 75,
    "preview": "[Desktop Entry]\nType=Application\nName=Web Browser\nExec=/usr/bin/firefox %u\n"
  },
  {
    "path": "mimetype/.local/share/applications/img.desktop",
    "chars": 77,
    "preview": "[Desktop Entry]\nType=Application\nName=Image viewer\nExec=/usr/bin/nsxiv -a %f\n"
  },
  {
    "path": "mimetype/.local/share/applications/pdf.desktop",
    "chars": 75,
    "preview": "[Desktop Entry]\nType=Application\nName=PDF reader\nExec=/usr/bin/firefox  %u\n"
  },
  {
    "path": "mimetype/.local/share/applications/text.desktop",
    "chars": 85,
    "preview": "[Desktop Entry]\nType=Application\nName=Text editor\nExec=/usr/bin/alacritty -e nvim %u\n"
  },
  {
    "path": "mimetype/.local/share/applications/video.desktop",
    "chars": 81,
    "preview": "[Desktop Entry]\nType=Application\nName=Video viewer\nExec=/usr/bin/mpv -quiet \"%u\"\n"
  },
  {
    "path": "mpv/.config/mpv/input.conf",
    "chars": 1254,
    "preview": "# Seeking\nl seek 5    # Forward\nh seek -5   # Rewind\n\n# Volume controle\nj add volume -2   # Decrease volume\nk add volume"
  },
  {
    "path": "mpv/.config/mpv/mpv.conf",
    "chars": 821,
    "preview": "# Adjusting the initial window size\ngeometry=36%\n\n# Disable On Screen Controlers\nosc=no\n\n# uosc provides its own seeking"
  },
  {
    "path": "mpv/.config/mpv/scripts/uosc.lua",
    "chars": 120715,
    "preview": "--[[\n\nuosc 2.16.0 - 2022-Mar-21 | https://github.com/darsain/uosc\n\nMinimalist cursor proximity based UI for MPV player.\n"
  },
  {
    "path": "nvim/.config/nvim/colors/idk.vim",
    "chars": 18848,
    "preview": "\" vi:syntax=vim\n\" \n\" Modified version of Base16 Tomorrow Night to tailored to my liking\n\" Original by Chris Kempson\n\" Mo"
  },
  {
    "path": "nvim/.config/nvim/colors/test.vim",
    "chars": 19277,
    "preview": "\" vi:syntax=vim\n\n\" base16-vim (https://github.com/chriskempson/base16-vim)\n\" by Chris Kempson (http://chriskempson.com)\n"
  },
  {
    "path": "nvim/.config/nvim/init.lua",
    "chars": 238,
    "preview": "local core_modules = {\n  \"options\",\n  \"plugins\",\n  \"mappings\",\n}\n\nfor _, module in ipairs(core_modules) do\n  local ok, e"
  },
  {
    "path": "nvim/.config/nvim/lua/mappings.lua",
    "chars": 3212,
    "preview": "local function map(mode, lhs, rhs, opts)\n    local options = {noremap = true}\n    if opts then\n        options = vim.tbl"
  },
  {
    "path": "nvim/.config/nvim/lua/options.lua",
    "chars": 1294,
    "preview": "local opt = vim.opt\nlocal g = vim.g\nlocal cmd = vim.cmd\n\nopt.relativenumber = true\nopt.lazyredraw = true\nopt.termguicolo"
  },
  {
    "path": "nvim/.config/nvim/lua/plugins/configs/bufferline.lua",
    "chars": 222,
    "preview": "local present, telescope = pcall(require, \"bufferline\")\nif not present then\n    return\nend\n\nrequire(\"bufferline\").setup "
  },
  {
    "path": "nvim/.config/nvim/lua/plugins/configs/lualine.lua",
    "chars": 246,
    "preview": "local present, telescope = pcall(require, \"lualine\")\nif not present then\n    return\nend\n\nrequire(\"lualine\").setup{\n    o"
  },
  {
    "path": "nvim/.config/nvim/lua/plugins/init.lua",
    "chars": 891,
    "preview": "local g = vim.g\nlocal fn = vim.fn\nlocal cmd = vim.cmd\n\nlocal packer_status_ok, packer = pcall(require, \"packer\")\nif not "
  },
  {
    "path": "other/.config/user-dirs.dirs",
    "chars": 611,
    "preview": "# This file is written by xdg-user-dirs-update\n# If you want to change or add directories, just edit the line you're\n# i"
  },
  {
    "path": "picom/.config/picom/picom.conf",
    "chars": 550,
    "preview": "# Prevent screen tearing\nbackend = \"glx\";\nvsync = true;\nglx-swap-method = 2;\nxrender-sync-fence = true;\n\n# Fade windows "
  },
  {
    "path": "polybar/.config/polybar/config.ini",
    "chars": 2623,
    "preview": "[bar/bar1]\n;------------;\n; DIMENSIONS ;\n;------------;\n\nwidth = 100%\nheight = 30\noffset-y = 0\noffset-x = 0\n\nborder-top-"
  },
  {
    "path": "polybar/.config/polybar/launch.sh",
    "chars": 387,
    "preview": "#!/bin/bash\n\n# Terminate already runnning bar instances\nkillall -q polybar\n\n# Wait until the processes have been shut do"
  },
  {
    "path": "polybar/.config/polybar/scripts/battery_widget.sh",
    "chars": 1083,
    "preview": "#!/usr/bin/env bash\nTIME_TO_EMPTY=$(upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E \"time\" | xargs | se"
  },
  {
    "path": "polybar/.config/polybar/scripts/bluetooth.sh",
    "chars": 454,
    "preview": "#!/usr/bin/env bash\nbt_connected_icon=\"\"\nbt_disconnected_icon=\"\"\n\ndevice_name=$(bluetoothctl info | sed -n \"s/Name: //"
  },
  {
    "path": "polybar/.config/polybar/scripts/mic_status.sh",
    "chars": 239,
    "preview": "#!/usr/bin/env bash\n\ncurrent_source=$(pactl info | grep \"Default Source\" | cut -f3 -d\" \")\nmic_is_on=$(pactl list sources"
  },
  {
    "path": "polybar/.config/polybar/scripts/today.sh",
    "chars": 125,
    "preview": "#!/bin/sh\n\nDATE=\"$(date +\"%a %d %H:%M\")\"\n\ncase \"$1\" in\n--calendar)\n    gnome-calendar\n    ;;\n*)\n    echo \"$DATE\"\n    ;;\n"
  },
  {
    "path": "polybar/.config/polybar/scripts/vpn-ip.sh",
    "chars": 549,
    "preview": "# TODO: add xclip checker and ability to copy the IP\n#       send notification upon copying\ninterface=\"$(ip tuntap show "
  },
  {
    "path": "polybar/.config/polybar/scripts/wifi_widget.sh",
    "chars": 124,
    "preview": "#!/usr/bin/env bash\n\ndunstify \"wifi\" \"IP: $(hostname --ip-address)\\nRouter: $(ip route show | awk '/default/ {print $3}'"
  },
  {
    "path": "rofi/.config/rofi/config.rasi",
    "chars": 210,
    "preview": "configuration {\n\tshow-icons: false;\n\tdrun-display-format: \"<b>{name}</b>\";\n\tcycle: false;\n\tsidebar-mode: false;\n\tm: \"-1\""
  },
  {
    "path": "rofi/.config/rofi/scripts/chars.txt",
    "chars": 303375,
    "preview": "😀 Grinning Face\n😁 Beaming Face With Smiling Eyes\n😂 Face With Tears of Joy\n🤣 Rolling on the Floor Laughing\n😃 Grinning Fac"
  },
  {
    "path": "rofi/.config/rofi/scripts/rofi-farge.sh",
    "chars": 222,
    "preview": "#!/usr/bin/env bash\nfor file in $(ls -tl /tmp/farge | cut -d \" \" -f 9); do\n    hex_code=$(echo $file | cut -d\".\" -f 1) \n"
  },
  {
    "path": "rofi/.config/rofi/scripts/rofi-finder.sh",
    "chars": 233,
    "preview": "selection=$(fd . --hidden --type file $HOME 2>/dev/null | \\\n    sed \"s;$HOME;~;\" | \\\n    rofi -sort -sorting-method fzf "
  },
  {
    "path": "rofi/.config/rofi/scripts/rofi-picker.sh",
    "chars": 212,
    "preview": "#!/usr/bin/env bash\nchar_file=\"$HOME/.config/rofi/scripts/chars.txt\"\nselection=\"$(cat \"$char_file\" | rofi -dmenu -i -p '"
  },
  {
    "path": "rofi/.config/rofi/themes/askpass.rasi",
    "chars": 1314,
    "preview": "/*\n * by Siddharth Dushantha 2020\n * A very minimal graphical helper for sudo's askpass.\n * \n * Preview: https://0x0.st/"
  },
  {
    "path": "rofi/.config/rofi/themes/default.rasi",
    "chars": 1524,
    "preview": "configuration {\n  font: \"JetBrainsMono Nerd Font Medium 11\";\n  kb-row-up: \"Up,Alt+k\";\n  kb-row-down: \"Down,Alt+j\";\n  kb-"
  },
  {
    "path": "rofi/.config/rofi/themes/run.rasi",
    "chars": 572,
    "preview": "configuration {\n  font: \"JetBrainsMono Nerd Font Medium 11\";\n\n  dmenu {\n    display-name: \"\";\n  }\n\n  timeout {\n    dela"
  },
  {
    "path": "vifm/.config/vifm/colors/Default.vifm",
    "chars": 3673,
    "preview": "\" You can edit this file by hand.\n\" The \" character at the beginning of a line comments out the line.\n\" Blank lines are "
  },
  {
    "path": "vifm/.config/vifm/colors/minimal.vifm",
    "chars": 1209,
    "preview": "\" colortheme\nhighlight clear\n\nhighlight Win cterm=none ctermfg=default ctermbg=none\nhighlight Directory cterm=bold cterm"
  },
  {
    "path": "vifm/.config/vifm/scripts/README",
    "chars": 378,
    "preview": "This directory is dedicated for user-supplied scripts/executables.\nvifm modifies its PATH environment variable to let us"
  },
  {
    "path": "vifm/.config/vifm/vifm-help.txt",
    "chars": 205894,
    "preview": "VIFM(1)\t\t\t    General Commands Manual\t\t       VIFM(1)\n\n\n\nNAME\n       vifm - vi file manager\n\nSYNOPSIS\n       vifm [OPTIO"
  },
  {
    "path": "vifm/.config/vifm/vifmrc",
    "chars": 12174,
    "preview": "\n\" {{{ General config \n\" This is the actual command used to start vi.  The default is vim.\n\" If you would like to use an"
  },
  {
    "path": "wget/.config/wgetrc",
    "chars": 45,
    "preview": "hsts-file = /home/siddharth/.cache/wget-hsts\n"
  },
  {
    "path": "zsh/.config/aliases",
    "chars": 4379,
    "preview": "#!/usr/bin/env bash\n\n# Quick shortcuts to some dirs\nalias dls=\"cd ~/downloads\"\nalias docs=\"cd ~/documents\"\nalias prjs=\"c"
  },
  {
    "path": "zsh/.zprofile",
    "chars": 2958,
    "preview": "# Start my graphical interface\nif [[ ! $DISPLAY && $XDG_VTNR -eq 1 ]]; then\n  exec startx > $HOME/.cache/startx.log 2>&1"
  },
  {
    "path": "zsh/.zshenv",
    "chars": 580,
    "preview": "# Allow all files in bin and the subdirs to be in PATH\nexport PATH=$PATH$( find $HOME/bin/ -type d -printf \":%p\" )\n\n# Al"
  },
  {
    "path": "zsh/.zshrc",
    "chars": 3971,
    "preview": "export PURE_PROMPT_SYMBOL=\"$\"\nzstyle ':prompt:*' color yellow\nexport PURE_PROMPT_VICMD_SYMBOL=\"$\"\n\n# Load my aliases \n[ "
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the sdushantha/dotfiles GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 106 files (754.3 KB), approximately 232.4k tokens, and a symbol index with 1 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!