commit a1780e0f078a090e741bead25cbdff80eaea42f1 Author: ooks-io Date: Mon Jul 24 19:20:20 2023 +1200 Initial commit diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7cb15f5 --- /dev/null +++ b/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1690084763, + "narHash": "sha256-Nw680m/pyVoosSgXZW415Z657mfVM2BxaxDPjEk48Z0=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "fb03fa5516d4e86059d24ab35a611ffa3a359547", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1690031011, + "narHash": "sha256-kzK0P4Smt7CL53YCdZCBbt9uBFFhE0iNvCki20etAf4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "12303c652b881435065a98729eb7278313041e49", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e93e57e --- /dev/null +++ b/flake.nix @@ -0,0 +1,43 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + home-manager.url = "github:nix-community/home-manager"; + home-manager.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { nixpkgs, home-manager, ... }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { + inherit system; + config = { + allowUnfree = true; + }; + }; + in + { + homeConfigurations = { + ooks = home-manager.lib.homeManagerConfiguration { + inherit pkgs; + extraSpecialArgs = { inherit nixpkgs system; }; + modules = [ + ./users/main/home.nix + ]; + }; + }; + nixosConfigurations = { + ooksthink = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + ./systems/laptop/laptop.nix + ]; + }; + ooksdesk = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + ./systems/desktop/configuration.nix + ]; + }; + }; + }; +} diff --git a/system/laptop/fonts.nix b/system/laptop/fonts.nix new file mode 100644 index 0000000..b258ebb --- /dev/null +++ b/system/laptop/fonts.nix @@ -0,0 +1,9 @@ + +{ pkgs, ... } +{ + +# Fonts + + font.fonts = with pkgs; [ + (nerdfonts.override { fonts = [ "JetBrainsMono" ]; }) + ]; diff --git a/system/laptop/hardware-configuration.nix b/system/laptop/hardware-configuration.nix new file mode 100644 index 0000000..f8b7c66 --- /dev/null +++ b/system/laptop/hardware-configuration.nix @@ -0,0 +1,61 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "usb_storage" "sd_mod" "rtsx_pci_sdmmc" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-uuid/db84a41f-6094-46b1-b98a-26e03afc18e1"; + fsType = "btrfs"; + options = [ "subvol=root" ]; + }; + + boot.initrd.luks.devices."cryptroot".device = "/dev/disk/by-uuid/3ea21f10-f705-457c-8366-a8268f658ba6"; + + fileSystems."/nix" = + { device = "/dev/disk/by-uuid/db84a41f-6094-46b1-b98a-26e03afc18e1"; + fsType = "btrfs"; + options = [ "subvol=nix" ]; + }; + + fileSystems."/persist" = + { device = "/dev/disk/by-uuid/db84a41f-6094-46b1-b98a-26e03afc18e1"; + fsType = "btrfs"; + options = [ "subvol=persist" ]; + }; + + fileSystems."/swap" = + { device = "/dev/disk/by-uuid/db84a41f-6094-46b1-b98a-26e03afc18e1"; + fsType = "btrfs"; + options = [ "subvol=swap" ]; + }; + + fileSystems."/boot" = + { device = "/dev/disk/by-uuid/45D8-8DC3"; + fsType = "vfat"; + }; + + swapDevices = [ ]; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.useDHCP = lib.mkDefault true; + # networking.interfaces.enp0s31f6.useDHCP = lib.mkDefault true; + # networking.interfaces.wlp4s0.useDHCP = lib.mkDefault true; + # networking.interfaces.wwp0s20f0u2c2.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/system/laptop/laptop.nix b/system/laptop/laptop.nix new file mode 100644 index 0000000..4024438 --- /dev/null +++ b/system/laptop/laptop.nix @@ -0,0 +1,288 @@ + + + + +{ config, pkgs, ... }: + +# Imports +# ------------------------------------------------------------------------------------------------- + +{ + imports = + [ # Include the results of the hardware scan + ./hardware-configuration.nix + ]; + +# Bootloader +# ------------------------------------------------------------------------------------------------- + + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + +# Nix Settings +# ------------------------------------------------------------------------------------------------- + + nix = { + settings = { + auto-optimise-store = true; + experimental-features = "nix-command flakes"; + }; + + +# Garbage Collection +# ------------------------------------------------------------------------------------------------- + + gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 2d"; + }; + }; + +# System Architecture +# ------------------------------------------------------------------------------------------------- + + + nixpkgs.system = "x86_64-linux"; + +# Allow Unfree +# ------------------------------------------------------------------------------------------------- + + nixpkgs.config.allowUnfree = true; + +# Networking +# ------------------------------------------------------------------------------------------------- + + networking = { + hostName = "ooksthink"; # Define your hostname. + networkmanager.enable = true; # Easiest to use and most distros use this by default. + }; + + +# Time Zone +# ------------------------------------------------------------------------------------------------- + + time.timeZone = "Pacific/Auckland"; + +# Localization +# ------------------------------------------------------------------------------------------------- + + i18n.defaultLocale = "en_US.UTF-8"; + +# X Server +# ------------------------------------------------------------------------------------------------- + + services.xserver = { + enable = true; + displayManager = { + defaultSession = null; + startx.enable = true; + }; + # displayManager.gdm = { + # enable = true; + # wayland = true; + # }; +}; + +# X11 Keymap +# ------------------------------------------------------------------------------------------------- + + # services.xserver.layout = "us"; + # services.xserver.xkbOptions = "eurosign:e,caps:escape"; + +# Printing +# ------------------------------------------------------------------------------------------------- + + # services.printing.enable = true; + +# Sound +# ------------------------------------------------------------------------------------------------- + + sound.enable = false; + hardware.pulseaudio.enable = false; + +# Touchpad +# ------------------------------------------------------------------------------------------------- + + # services.xserver.libinput.enable = true; + +# User +# ------------------------------------------------------------------------------------------------- + + users.users = { + ooks = { + isNormalUser = true; + extraGroups = [ "wheel" ]; + shell = pkgs.fish; + +# User Packages +# ------------------------------------------------------------------------------------------------- + + packages = with pkgs; [ + firefox + tree + hyprland + kitty + ]; + }; + }; + +# System Environment +# ------------------------------------------------------------------------------------------------- + + environment = { + binsh = "${pkgs.dash}/bin/dash"; + shells = with pkgs; [ fish ]; + systemPackages = with pkgs; [ + # Editor + # ------ + neovim + # Utility + # ------ + wget + dash + neofetch + glib + xdg-utils + pciutils + gdb + killall + jetbrains-mono + cargo + p7zip + joshuto + zip + rar + btop + git + libnotify + dunst + wl-clipboard + wlr-randr + wayland + wayland-scanner + wayland-utils + egl-wayland + wayland-protocols + wev + alsa-lib + alsa-utils + flac + pulsemixer + linux-firmware + lxappearance + pkgs.sway-contrib.grimshot + flameshot + grim + ]; + }; + +# Fonts +# ------------------------------------------------------------------------------------------------- + +fonts.fonts = with pkgs; [ + (nerdfonts.override { fonts = [ "JetBrainsMono" ]; }) + ]; + +# Programs +# ------------------------------------------------------------------------------------------------- + + programs.mtr.enable = true; + programs.gnupg.agent = { + enable = true; + enableSSHSupport = true; + }; + programs.hyprland = { + enable = true; + xwayland.enable = true; + }; + programs.fish = { + enable = true; + }; + +# Services +# ------------------------------------------------------------------------------------------------- + + security.rtkit.enable = true; + + services = { + pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + jack.enable = true; + wireplumber.enable = true; + }; + dbus.packages = [ pkgs.gcr ]; + getty.autologinUser = "ooks"; + auto-cpufreq = { + enable = true; + settings = { + battery = { + governor = "powersave"; + turbo = "never"; + }; + charger = { + governor = "performance"; + turbo = "auto"; + }; + }; + }; + + }; + + systemd = { + user.services.polkit-gnome-authentication-agent-1 = { + description = "polkit-gnome-authentication-agent-1"; + wantedBy = [ "graphical-session.target" ]; + wants = [ "graphical-session.target" ]; + after = [ "graphical-session.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1"; + Restart = "on-failure"; + RestartSec = 1; + TimeoutStopSec = 10; + }; + }; + }; + +# Security +# ------------------------------------------------------------------------------------------------- + + security.polkit.enable = true; + security.sudo = { + enable = true; + extraConfig = '' + ooks ALL=(ALL) NOPASSWD:ALL + ''; + }; + + +# D-Bus +# ------------------------------------------------------------------------------------------------- + + services.dbus.enable = true; + +# Firewall +# ------------------------------------------------------------------------------------------------- + + # networking.firewall.allowedTCPPorts = [ ... ]; + # networking.firewall.allowedUDPPorts = [ ... ]; + # Or disable the firewall altogether. + # networking.firewall.enable = false; + +# System Version +# ------------------------------------------------------------------------------------------------- + + system = { + autoUpgrade = { + enable = false; + channel = "https://nixos.org/channels/nix-unstable"; + }; + stateVersion = "23.11"; + copySystemConfiguration = false; + }; +} diff --git a/system/laptop/packages.nix b/system/laptop/packages.nix new file mode 100644 index 0000000..131975a --- /dev/null +++ b/system/laptop/packages.nix @@ -0,0 +1,78 @@ + +{ pkgs, ... } +{ +# System Packages +# ------------------------------------------------------------------------------------------------- + + environment = { + binsh = with pkgs; [ fish ] + systemPackages = with pkgs; [ + # Editor + neovim + # Utility + wget + neofetch + glib + xdg-utils + killall + zip + rar + btop + p7zip + git + pciutils + gdb + dash + curl + # Programming + cargo + # Fonts + jetbrains-mono + # File browsers + ranger + joshuto + # Wayland + wayland + wayland-scanner + wayland-utils + egl-wayland + wayland-protocols + wev # Wayland window debugger + wl-clipboard # Wayland clipboard + wlr-randr + # Firmware + linux-firmware + # Audio + alsa-lib + alsa-utils + flac + pulsemixer + # Appearance + lxappearance + # Screenshot + pkgs.sway-contrib.grimshot + flameshot + grim + # Notification + dunst + libnotify + ]; + }; + +# Programs +# ------------------------------------------------------------------------------------------------- + + programs.mtr.enable = true + programs.gnupg.agent = { + enable = true; + enabeSSHSupport = true; + }; + programs.hyprland = { + enable = true; + xwayland.enable = true; + }; + programs.fish = { + enable = true + }; + + diff --git a/user/ooks/home.nix b/user/ooks/home.nix new file mode 100644 index 0000000..835ec37 --- /dev/null +++ b/user/ooks/home.nix @@ -0,0 +1,70 @@ +{ config, pkgs, ... }: + +{ + # Home Manager needs a bit of information about you and the paths it should + # manage. + home.username = "ooks"; + home.homeDirectory = "/home/ooks"; + + # This value determines the Home Manager release that your configuration is + # compatible with. This helps avoid breakage when a new Home Manager release + # introduces backwards incompatible changes. + # + # You should not change this value, even if you update Home Manager. If you do + # want to update the value, then make sure to first check the Home Manager + # release notes. + home.stateVersion = "23.05"; # Please read the comment before changing. + + # The home.packages option allows you to install Nix packages into your + # environment. + home.packages = [ + # # Adds the 'hello' command to your environment. It prints a friendly + # # "Hello, world!" when run. + # pkgs.hello + + # # It is sometimes useful to fine-tune packages, for example, by applying + # # overrides. You can do that directly here, just don't forget the + # # parentheses. Maybe you want to install Nerd Fonts with a limited number of + # # fonts? + # (pkgs.nerdfonts.override { fonts = [ "FantasqueSansMono" ]; }) + + # # You can also create simple shell scripts directly inside your + # # configuration. For example, this adds a command 'my-hello' to your + # # environment: + # (pkgs.writeShellScriptBin "my-hello" '' + # echo "Hello, ${config.home.username}!" + # '') + ]; + + # Home Manager is pretty good at managing dotfiles. The primary way to manage + # plain files is through 'home.file'. + home.file = { + # # Building this configuration will create a copy of 'dotfiles/screenrc' in + # # the Nix store. Activating the configuration will then make '~/.screenrc' a + # # symlink to the Nix store copy. + # ".screenrc".source = dotfiles/screenrc; + + # # You can also set the file content immediately. + # ".gradle/gradle.properties".text = '' + # org.gradle.console=verbose + # org.gradle.daemon.idletimeout=3600000 + # ''; + }; + + # You can also manage environment variables but you will have to manually + # source + # + # ~/.nix-profile/etc/profile.d/hm-session-vars.sh + # + # or + # + # /etc/profiles/per-user/ooks/etc/profile.d/hm-session-vars.sh + # + # if you don't want to manage your shell through Home Manager. + home.sessionVariables = { + # EDITOR = "emacs"; + }; + + # Let Home Manager install and manage itself. + programs.home-manager.enable = true; +} diff --git a/user/ooks/modules/desktop/hyprland/home.nix b/user/ooks/modules/desktop/hyprland/home.nix new file mode 100644 index 0000000..4b4f501 --- /dev/null +++ b/user/ooks/modules/desktop/hyprland/home.nix @@ -0,0 +1,198 @@ + config, lib, pkgs, ... }: + +{ + imports = [ + (import ../../environment/hypr-variable.nix) + ]; + programs = { + bash = { + initExtra = '' + if [ -z $DISPLAY ] && [ "$(tty)" = "/dev/tty1" ]; then + exec Hyprland + fi + ''; + }; + fish = { + loginShellInit = '' + set TTY1 (tty) + [ "$TTY1" = "/dev/tty1" ] && exec Hyprland + ''; + }; + }; + systemd.user.targets.hyprland-session.Unit.Wants = [ "xdg-desktop-autostart.target" ]; + wayland.windowManager.hyprland = { + enable = true; + systemdIntegration = true; + nvidiaPatches = false; + extraConfig = '' + monitor=,preferred,auto,auto + + exec-once = swaybg -i ~/.dotfiles/walls/everforest/megacity.png + + input { + kb_layout = us + kb_variant = + kb_model = + kb_options = + kb_rules = + + follow_mouse = 1 + + touchpad { + natural_scroll = yes + } + + sensitivity = 0 # -1.0 - 1.0, 0 means no modification. + } + + general { + + gaps_in = 5 + gaps_out = 5 + border_size = 2 + col.active_border = 0xffA7C080 + col.inactive_border = rgba(595959aa) + + layout = dwindle + } + + decoration { + # See https://wiki.hyprland.org/Configuring/Variables/ for more + + rounding = 5 + multisample_edges = true + blur = no + blur_size = 3 + blur_passes = 1 + blur_new_optimizations = on + + drop_shadow = no + shadow_range = 4 + shadow_render_power = 3 + col.shadow = rgba(1a1a1aee) + } + + misc { + animate_manual_resizes = false + enable_swallow = true + swallow_regex = ^(kitty)$ + focus_on_activate = true + disable_hyprland_logo = true + } +# Animations +# ------------------------------------------------------------------------------------------------- + + animations { + enabled = yes + bezier = overshot, 0.11, 1, 0.36, 1 + animation = windows, 1, 4, overshot, slide + animation = windowsOut, 1, 5, default, popin 80% + animation = border, 1, 5, default + animation = fade, 1, 8, default + animation = workspaces, 1, 6, overshot, slide + } + +# Layout +# ------------------------------------------------------------------------------------------------- + + } + + dwindle { + pseudotile = yes + preserve_split = yes + } + + master { + new_is_master = true + } + +# Gestures +# ------------------------------------------------------------------------------------------------- + + gestures { + workspace_swipe = off + } + +# Device Config +# ------------------------------------------------------------------------------------------------- + + device:epic-mouse-v1 { + sensitivity = -0.5 + } + +# Window Rules +# ------------------------------------------------------------------------------------------------- + +# Example windowrule v1 +# windowrule = float, ^(kitty)$ +# Example windowrule v2 +# windowrulev2 = float,class:^(kitty)$,title:^(kitty)$ +# See https://wiki.hyprland.org/Configuring/Window-Rules/ for more + +# Main Mod +# ------------------------------------------------------------------------------------------------- + + $mainMod = SUPER + +# Program Binds +# ------------------------------------------------------------------------------------------------- + + bind = $mainMod, return, exec, kitty + bind = $mainMod, Q, killactive, + bind = $mainMod, B, exec, firefox + bind = $mainMod, M, exit, + bind = $mainMod, space, togglefloating, + bind = $mainMod, P, pseudo, # dwindle + bind = $mainMod, J, togglesplit, # dwindle + +# Move focus with mainMod + arrow keys + bind = $mainMod, left, movefocus, l + bind = $mainMod, right, movefocus, r + bind = $mainMod, up, movefocus, u + bind = $mainMod, down, movefocus, d + +# Workspaces +# ------------------------------------------------------------------------------------------------- + + bind = $mainMod, 1, workspace, 1 + bind = $mainMod, 2, workspace, 2 + bind = $mainMod, 3, workspace, 3 + bind = $mainMod, 4, workspace, 4 + bind = $mainMod, 5, workspace, 5 + bind = $mainMod, 6, workspace, 6 + bind = $mainMod, 7, workspace, 7 + bind = $mainMod, 8, workspace, 8 + bind = $mainMod, 9, workspace, 9 + bind = $mainMod, 0, workspace, 10 + binds { + workspace_back_and_forth = 1 + allow_workspace_cycles = 1 + } + bind=$mainMod,tab,workspace,previous + +# Move To Workspaces +# ------------------------------------------------------------------------------------------------- + + bind = $mainMod SHIFT, 1, movetoworkspace, 1 + bind = $mainMod SHIFT, 2, movetoworkspace, 2 + bind = $mainMod SHIFT, 3, movetoworkspace, 3 + bind = $mainMod SHIFT, 4, movetoworkspace, 4 + bind = $mainMod SHIFT, 5, movetoworkspace, 5 + bind = $mainMod SHIFT, 6, movetoworkspace, 6 + bind = $mainMod SHIFT, 7, movetoworkspace, 7 + bind = $mainMod SHIFT, 8, movetoworkspace, 8 + bind = $mainMod SHIFT, 9, movetoworkspace, 9 + bind = $mainMod SHIFT, 0, movetoworkspace, 10 + +# Workspaces Scroll +# ------------------------------------------------------------------------------------------------- + + bind = $mainMod, mouse_down, workspace, e+1 + bind = $mainMod, mouse_up, workspace, e-1 + +# Move/resize windows with mainMod + LMB/RMB and dragging + bindm = $mainMod, mouse:272, movewindow + bindm = $mainMod, mouse:273, resizewindow + ''; + }; + } diff --git a/user/ooks/modules/environment/hypr-variable.nix b/user/ooks/modules/environment/hypr-variable.nix new file mode 100644 index 0000000..0a1ed5c --- /dev/null +++ b/user/ooks/modules/environment/hypr-variable.nix @@ -0,0 +1,39 @@ +{ config, pkgs, ... }: + +{ + home = { + sessionVariables = { + EDITOR = "nvim"; + BROWSER = "firefox"; + TERMINAL = "kitty"; + # GTK_IM_MODULE = "fcitx5"; + # QT_IM_MODULE = "fcitx5"; + # XMODIFIERS = "@im=fcitx5"; + QT_QPA_PLATFORMTHEME = "gtk3"; + QT_SCALE_FACTOR = "1"; + MOZ_ENABLE_WAYLAND = "1"; + SDL_VIDEODRIVER = "wayland"; + _JAVA_AWT_WM_NONREPARENTING = "1"; + QT_QPA_PLATFORM = "wayland"; + QT_WAYLAND_DISABLE_WINDOWDECORATION = "1"; + QT_AUTO_SCREEN_SCALE_FACTOR = "1"; + WLR_DRM_DEVICES = "/dev/dri/card1:/dev/dri/card0"; + WLR_NO_HARDWARE_CURSORS = "1"; # if no cursor,uncomment this line + WLR_RENDERER_ALLOW_SOFTWARE = "1"; + # GBM_BACKEND = "nvidia-drm"; + CLUTTER_BACKEND = "wayland"; + __GLX_VENDOR_LIBRARY_NAME = "nvidia"; + LIBVA_DRIVER_NAME = "nvidia"; + WLR_RENDERER = "vulkan"; + # __NV_PRIME_RENDER_OFFLOAD = "1"; + + XDG_CURRENT_DESKTOP = "Hyprland"; + XDG_SESSION_DESKTOP = "Hyprland"; + XDG_SESSION_TYPE = "wayland"; + XDG_CACHE_HOME = "\${HOME}/.cache"; + XDG_CONFIG_HOME = "\${HOME}/.config"; + XDG_BIN_HOME = "\${HOME}/.local/bin"; + XDG_DATA_HOME = "\${HOME}/.local/share"; + }; + }; +} diff --git a/user/ooks/modules/programs/default.nix b/user/ooks/modules/programs/default.nix new file mode 100644 index 0000000..a3ccf25 --- /dev/null +++ b/user/ooks/modules/programs/default.nix @@ -0,0 +1,16 @@ +[ + ./joshuto/ + ./kitty/ + ./lazygit/ + ./neofetch/ + ./resource-monitor/ + ./search/ + ./starship/ + ./youtube-tui/ + ./yt-dlp/ + ./zathura/ + ./firefox/ + ./imgview/ + ./notify/ + ./mpv/ +] diff --git a/user/ooks/modules/programs/firefox/default.nix b/user/ooks/modules/programs/firefox/default.nix new file mode 100644 index 0000000..f11e97e --- /dev/null +++ b/user/ooks/modules/programs/firefox/default.nix @@ -0,0 +1,14 @@ +{ config, pkgs, ... }: + +{ + programs.firefox = { + enable = true; + extraPolicies = { + DisplayBookmarksToolbar = true; + Preferences = { + "browser.toolbars.bookmarks.visibility" = "never"; + "toolkit.legacyUserProfileCustomizations.stylesheets" = true; + "media.ffmpeg.vaapi.enabled" = true; + }; + }; + }; diff --git a/user/ooks/modules/programs/imgview/default.nix b/user/ooks/modules/programs/imgview/default.nix new file mode 100644 index 0000000..dcc54fe --- /dev/null +++ b/user/ooks/modules/programs/imgview/default.nix @@ -0,0 +1,8 @@ +{ config, pkgs, ... }: +{ + home = { + packages = with pkgs; [ + imv + ]; + }; +} diff --git a/user/ooks/modules/programs/joshuto/config/bookmarks.toml b/user/ooks/modules/programs/joshuto/config/bookmarks.toml new file mode 100644 index 0000000..a0764e2 --- /dev/null +++ b/user/ooks/modules/programs/joshuto/config/bookmarks.toml @@ -0,0 +1,6 @@ +bookmark = [ + { key = "r", path = "/" }, + { key = "e", path = "/etc" }, + + { key = "h", path = "~/" }, +] diff --git a/user/ooks/modules/programs/joshuto/config/joshuto.toml b/user/ooks/modules/programs/joshuto/config/joshuto.toml new file mode 100644 index 0000000..8154eaa --- /dev/null +++ b/user/ooks/modules/programs/joshuto/config/joshuto.toml @@ -0,0 +1,38 @@ +numbered_command = false + +use_trash = false +watch_files = true +xdg_open = false +xdg_open_fork = false + + +[display] +# default, hsplit +mode = "default" + +automatically_count_files = false +collapse_preview = true +# ratios for parent view (optional), current view and preview +column_ratio = [2, 3, 5] +scroll_offset = 6 +show_borders = true +show_hidden = false +show_icons = true +tilde_in_titlebar = true +# none, absolute, relative +line_number_style = "none" + +[display.sort] +# lexical, mtime, natural +method = "natural" +case_sensitive = false +directories_first = true +reverse = false + +[preview] +max_preview_size = 2097152 # 2MB +preview_script = "~/.config/joshuto/preview_file.sh" + +[tab] +# inherit, home, root +home_page = "home" diff --git a/user/ooks/modules/programs/joshuto/config/keymap.toml b/user/ooks/modules/programs/joshuto/config/keymap.toml new file mode 100644 index 0000000..ccb2ed1 --- /dev/null +++ b/user/ooks/modules/programs/joshuto/config/keymap.toml @@ -0,0 +1,174 @@ +[default_view] + +keymap = [ + { keys = ["escape"], command = "escape" }, + { keys = ["ctrl+t"], command = "new_tab" }, + { keys = ["alt+t"], command = "new_tab --cursor" }, + { keys = ["T"], command = "new_tab --current" }, + { keys = ["W"], command = "close_tab" }, + { keys = ["ctrl+w"], command = "close_tab" }, + { keys = ["q"], command = "close_tab" }, + { keys = ["ctrl+c"], command = "quit" }, + { keys = ["Q"], command = "quit --output-current-directory" }, + + { keys = ["R"], command = "reload_dirlist" }, + { keys = ["z", "h"], command = "toggle_hidden" }, + { keys = ["ctrl+h"], command = "toggle_hidden" }, + { keys = ["backspace"], command = "toggle_hidden" }, + { keys = ["\t"], command = "tab_switch 1" }, + { keys = ["backtab"], command = "tab_switch -1" }, + + { keys = ["alt+1"], command = "tab_switch_index 1" }, + { keys = ["alt+2"], command = "tab_switch_index 2" }, + { keys = ["alt+3"], command = "tab_switch_index 3" }, + { keys = ["alt+4"], command = "tab_switch_index 4" }, + { keys = ["alt+5"], command = "tab_switch_index 5" }, + + { keys = ["1"], command = "numbered_command 1" }, + { keys = ["2"], command = "numbered_command 2" }, + { keys = ["3"], command = "numbered_command 3" }, + { keys = ["4"], command = "numbered_command 4" }, + { keys = ["5"], command = "numbered_command 5" }, + { keys = ["6"], command = "numbered_command 6" }, + { keys = ["7"], command = "numbered_command 7" }, + { keys = ["8"], command = "numbered_command 8" }, + { keys = ["9"], command = "numbered_command 9" }, + + # arrow keys + { keys = ["arrow_up"], command = "cursor_move_up" }, + { keys = ["arrow_down"], command = "cursor_move_down" }, + { keys = ["arrow_left"], command = "cd .." }, + { keys = ["arrow_right"], command = "open" }, + { keys = ["\n"], command = "open" }, + { keys = ["home"], command = "cursor_move_home" }, + { keys = ["end"], command = "cursor_move_end" }, + { keys = ["page_up"], command = "cursor_move_page_up" }, + { keys = ["page_down"], command = "cursor_move_page_down" }, + { keys = ["ctrl+u"], command = "cursor_move_page_up 0.5" }, + { keys = ["ctrl+d"], command = "cursor_move_page_down 0.5" }, + + # vim-like keybindings + { keys = ["j"], command = "cursor_move_down" }, + { keys = ["k"], command = "cursor_move_up" }, + { keys = ["h"], command = "cd .." }, + { keys = ["l"], command = "open" }, + { keys = ["g", "g"], command = "cursor_move_home" }, + { keys = ["G"], command = "cursor_move_end" }, + { keys = ["r"], command = "open_with" }, + + { keys = ["H"], command = "cursor_move_page_home" }, + { keys = ["L"], command = "cursor_move_page_middle" }, + { keys = ["M"], command = "cursor_move_page_end" }, + + { keys = ["["], command = "parent_cursor_move_up" }, + { keys = ["]"], command = "parent_cursor_move_down" }, + + { keys = ["c", "d"], command = ":cd " }, + { keys = ["d", "d"], command = "cut_files" }, + { keys = ["y", "y"], command = "copy_files" }, + { keys = ["y", "n"], command = "copy_filename" }, + { keys = ["y", "."], command = "copy_filename_without_extension" }, + { keys = ["y", "p"], command = "copy_filepath" }, + { keys = ["y", "d"], command = "copy_dirpath" }, + + { keys = ["p", "l"], command = "symlink_files --relative=false" }, + { keys = ["p", "L"], command = "symlink_files --relative=true" }, + + { keys = ["delete"], command = "delete_files" }, + { keys = ["d", "D"], command = "delete_files" }, + + { keys = ["p", "p"], command = "paste_files" }, + { keys = ["p", "o"], command = "paste_files --overwrite=true" }, + + { keys = ["a"], command = "rename_append" }, + { keys = ["A"], command = "rename_prepend" }, + + { keys = ["f", "t"], command = ":touch " }, + + { keys = [" "], command = "select --toggle=true" }, + { keys = ["t"], command = "select --all=true --toggle=true" }, + { keys = ["V"], command = "toggle_visual" }, + + { keys = ["w"], command = "show_tasks --exit-key=w" }, + { keys = ["b", "b"], command = "bulk_rename" }, + { keys = ["="], command = "set_mode" }, + + { keys = [":"], command = ":" }, + { keys = [";"], command = ":" }, + + { keys = ["'"], command = ":shell " }, + { keys = ["m", "k"], command = ":mkdir " }, + { keys = ["c", "w"], command = ":rename " }, + + { keys = ["/"], command = ":search " }, + { keys = ["|"], command = ":search_inc " }, + { keys = ["\\"], command = ":search_glob " }, + { keys = ["ctrl+f"], command = "search_fzf" }, + { keys = ["C"], command = "subdir_fzf" }, + + { keys = ["n"], command = "search_next" }, + { keys = ["N"], command = "search_prev" }, + + { keys = ["s", "r"], command = "sort reverse" }, + { keys = ["s", "l"], command = "sort lexical" }, + { keys = ["s", "m"], command = "sort mtime" }, + { keys = ["s", "n"], command = "sort natural" }, + { keys = ["s", "s"], command = "sort size" }, + { keys = ["s", "e"], command = "sort ext" }, + + { keys = ["m", "s"], command = "linemode size" }, + { keys = ["m", "m"], command = "linemode mtime" }, + { keys = ["m", "M"], command = "linemode sizemtime" }, + + { keys = ["g", "r"], command = "cd /" }, + { keys = ["g", "c"], command = "cd ~/.config" }, + { keys = ["g", "d"], command = "cd ~/Downloads" }, + { keys = ["g", "e"], command = "cd /etc" }, + { keys = ["g", "h"], command = "cd ~/" }, + { keys = ["g", "f"], command = "cd ~/Flakes" }, + { keys = ["?"], command = "help" }, + + # `Shift + s` enter shell + { keys = ["S"], command = "shell fish" }, + #youtube-dl + { keys = ["y", "a"], command = ":shell yt-dlp -x --audio-format mp3 " }, + { keys = ["y", "v"], command = ":shell yt-dlp -x -ic " }, +] + +[task_view] + +keymap = [ + # arrow keys + { keys = ["arrow_up"], command = "cursor_move_up" }, + { keys = ["arrow_down"], command = "cursor_move_down" }, + { keys = ["home"], command = "cursor_move_home" }, + { keys = ["end"], command = "cursor_move_end" }, + + # vim-like keybindings + { keys = ["j"], command = "cursor_move_down" }, + { keys = ["k"], command = "cursor_move_up" }, + { keys = ["g", "g"], command = "cursor_move_home" }, + { keys = ["G"], command = "cursor_move_end" }, + + { keys = ["w"], command = "show_tasks" }, + { keys = ["escape"], command = "show_tasks" }, +] + +[help_view] + +keymap = [ + # arrow keys + { keys = ["arrow_up"], command = "cursor_move_up" }, + { keys = ["arrow_down"], command = "cursor_move_down" }, + { keys = ["home"], command = "cursor_move_home" }, + { keys = ["end"], command = "cursor_move_end" }, + + # vim-like keybindings + { keys = ["j"], command = "cursor_move_down" }, + { keys = ["k"], command = "cursor_move_up" }, + { keys = ["g", "g"], command = "cursor_move_home" }, + { keys = ["G"], command = "cursor_move_end" }, + + { keys = ["w"], command = "show_tasks" }, + { keys = ["escape"], command = "show_tasks" }, +] diff --git a/user/ooks/modules/programs/joshuto/config/mimetype.toml b/user/ooks/modules/programs/joshuto/config/mimetype.toml new file mode 100644 index 0000000..7005b2c --- /dev/null +++ b/user/ooks/modules/programs/joshuto/config/mimetype.toml @@ -0,0 +1,229 @@ +[class] +audio_default = [ + { command = "mpv", args = [ + "--", + ] }, + { command = "mediainfo", confirm_exit = true }, +] + +image_default = [ + { command = "qimgv", args = [ + "--", + ], fork = true, silent = true }, + { command = "krita", args = [ + "--", + ], fork = true, silent = true }, + { command = "exiftool", confirm_exit = true }, + { command = "swappy", args = [ + "-f", + ], fork = true }, +] + +video_default = [ + { command = "mpv", args = [ + "--", + ], fork = true, silent = true }, + { command = "mediainfo", confirm_exit = true }, + { command = "mpv", args = [ + "--mute", + "on", + "--", + ], fork = true, silent = true }, +] + +text_default = [ + { command = "micro" }, + { command = "gedit", fork = true, silent = true }, + { command = "bat", args = [ + "--paging=always", + ] }, +] + +reader_default = [{ command = "evince", fork = true, silent = true }] + +libreoffice_default = [{ command = "libreoffice", fork = true, silent = true }] + +[extension] + +## image formats +avif.inherit = "image_default" +bmp.inherit = "image_default" +gif.inherit = "image_default" +heic.inherit = "image_default" +jpeg.inherit = "image_default" +jpe.inherit = "image_default" +jpg.inherit = "image_default" +pgm.inherit = "image_default" +png.inherit = "image_default" +ppm.inherit = "image_default" +webp.inherit = "image_default" + +svg.app_list = [ + { command = "inkview", fork = true, silent = true }, + { command = "inkscape", fork = true, silent = true }, +] +tiff.app_list = [ + { command = "qimgv", fork = true, silent = true }, + { command = "krita", fork = true, silent = true }, +] + +## audio formats +flac.inherit = "audio_default" +m4a.inherit = "audio_default" +mp3.inherit = "audio_default" +ogg.inherit = "audio_default" +wav.inherit = "audio_default" + +## video formats +avi.inherit = "video_default" +av1.inherit = "video_default" +flv.inherit = "video_default" +mkv.inherit = "video_default" +m4v.inherit = "video_default" +mov.inherit = "video_default" +mp4.inherit = "video_default" +ts.inherit = "video_default" +webm.inherit = "video_default" +wmv.inherit = "video_default" + +## text formats +build.inherit = "text_default" +c.inherit = "text_default" +cmake.inherit = "text_default" +conf.inherit = "text_default" +cpp.inherit = "text_default" +css.inherit = "text_default" +csv.inherit = "text_default" +cu.inherit = "text_default" +ebuild.inherit = "text_default" +eex.inherit = "text_default" +env.inherit = "text_default" +ex.inherit = "text_default" +exs.inherit = "text_default" +go.inherit = "text_default" +h.inherit = "text_default" +hpp.inherit = "text_default" +hs.inherit = "text_default" +html.inherit = "text_default" +ini.inherit = "text_default" +java.inherit = "text_default" +js.inherit = "text_default" +json.inherit = "text_default" +kt.inherit = "text_default" +lua.inherit = "text_default" +log.inherit = "text_default" +md.inherit = "text_default" +micro.inherit = "text_default" +ninja.inherit = "text_default" +py.inherit = "text_default" +rkt.inherit = "text_default" +rs.inherit = "text_default" +scss.inherit = "text_default" +sh.inherit = "text_default" +srt.inherit = "text_default" +svelte.inherit = "text_default" +toml.inherit = "text_default" +tsx.inherit = "text_default" +txt.inherit = "text_default" +vim.inherit = "text_default" +xml.inherit = "text_default" +yaml.inherit = "text_default" +yml.inherit = "text_default" + +# archive formats +7z.app_list = [ + { command = "7z", args = [ + "x", + ], confirm_exit = true }, + { command = "file-roller", fork = true, silent = true }, +] +bz2.app_list = [ + { command = "tar", args = [ + "-xvjf", + ], confirm_exit = true }, + { command = "file-roller", fork = true, silent = true }, +] +gz.app_list = [ + { command = "tar", args = [ + "-xvzf", + ], confirm_exit = true }, + { command = "file-roller", fork = true, silent = true }, +] +tar.app_list = [ + { command = "tar", args = [ + "-xvf", + ], confirm_exit = true }, + { command = "file-roller", fork = true, silent = true }, +] +tgz.app_list = [ + { command = "tar", args = [ + "-xvzf", + ], confirm_exit = true }, + { command = "file-roller", fork = true, silent = true }, +] +rar.app_list = [ + { command = "unrar", args = [ + "x", + ], confirm_exit = true }, + { command = "file-roller", fork = true, silent = true }, +] +xz.app_list = [ + { command = "tar", args = [ + "-xvJf", + ], confirm_exit = true }, + { command = "file-roller", fork = true, silent = true }, +] +zip.app_list = [ + { command = "unzip", confirm_exit = true }, + { command = "file-roller", fork = true, silent = true }, +] + +# misc formats +aup.app_list = [{ command = "audacity", fork = true, silent = true }] + +m3u.app_list = [ + { command = "micro" }, + { command = "mpv" }, + { command = "gedit", fork = true, silent = true }, + { command = "bat", confirm_exit = true }, +] + +odt.inherit = "libreoffice_default" +odf.inherit = "libreoffice_default" +ods.inherit = "libreoffice_default" +odp.inherit = "libreoffice_default" + +doc.inherit = "libreoffice_default" +docx.inherit = "libreoffice_default" +xls.inherit = "libreoffice_default" +xlsx.inherit = "libreoffice_default" +ppt.inherit = "libreoffice_default" +pptx.inherit = "libreoffice_default" + +pdf.inherit = "reader_default" + +kra.app_list = [{ command = "krita", fork = true, silent = true }] +kdenlive.app_list = [{ command = "kdenlive", fork = true, silent = true }] + +tex.app_list = [ + { command = "micro" }, + { command = "gedit", fork = true, silent = true }, + { command = "bat", confirm_exit = true }, + { command = "pdflatex" }, +] + +torrent.app_list = [{ command = "transmission-gtk" }] + +[mimetype] + +# application/octet-stream +[mimetype.application.subtype.octet-stream] +inherit = "video_default" + +# text/* +[mimetype.text] +inherit = "text_default" + +# text/* +[mimetype.video] +inherit = "video_default" diff --git a/user/ooks/modules/programs/joshuto/config/preview_file.sh b/user/ooks/modules/programs/joshuto/config/preview_file.sh new file mode 100644 index 0000000..a6940c6 --- /dev/null +++ b/user/ooks/modules/programs/joshuto/config/preview_file.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash + +## This script is a template script for creating textual file previews in Joshuto. +## +## Copy this script to your Joshuto configuration directory and refer to this +## script in `joshuto.toml` in the `[preview]` section like +## ``` +## preview_script = "~/.config/joshuto/preview_file.sh" +## ``` +## Joshuto will call this script for each file when first hovered by the cursor. +## If this script returns with an exit code 0, the stdout of this script will be +## the file's preview text in Joshuto's right panel. +## The preview text will be cached by Joshuto and only renewed on reload. +## ANSI color codes are supported if Joshuto is build with the `syntax_highlight` +## feature. +## +## This script is considered a configuration file and must be updated manually. +## It will be left untouched if you upgrade Joshuto. +## +## Meanings of exit codes: +## +## code | meaning | action of ranger +## -----+------------+------------------------------------------- +## 0 | success | Display stdout as preview +## 1 | no preview | Display no preview at all +## +## This script is used only as a provider for textual previews. +## Image previews are independent from this script. +## + +IFS=$'\n' + +# Security measures: +# * noclobber prevents you from overwriting a file with `>` +# * noglob prevents expansion of wild cards +# * nounset causes bash to fail if an undeclared variable is used (e.g. typos) +# * pipefail causes a pipeline to fail also if a command other than the last one fails +set -o noclobber -o noglob -o nounset -o pipefail + +FILE_PATH="" +PREVIEW_WIDTH=10 +PREVIEW_HEIGHT=10 + +while [ "$#" -gt 0 ]; do + case "$1" in + "--path") + shift + FILE_PATH="$1" + ;; + "--preview-width") + shift + PREVIEW_WIDTH="$1" + ;; + "--preview-height") + shift + PREVIEW_HEIGHT="$1" + ;; + esac + shift +done + +handle_extension() { + case "${FILE_EXTENSION_LOWER}" in + ## Archive + a|ace|alz|arc|arj|bz|bz2|cab|cpio|deb|gz|jar|lha|lz|lzh|lzma|lzo|\ + rpm|rz|t7z|tar|tbz|tbz2|tgz|tlz|txz|tZ|tzo|war|xpi|xz|Z|zip) + atool --list -- "${FILE_PATH}" && exit 0 + bsdtar --list --file "${FILE_PATH}" && exit 0 + exit 1 ;; + rar) + ## Avoid password prompt by providing empty password + unrar lt -p- -- "${FILE_PATH}" && exit 0 + exit 1 ;; + 7z) + ## Avoid password prompt by providing empty password + 7z l -p -- "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## PDF + pdf) + ## Preview as text conversion + pdftotext -l 10 -nopgbrk -q -- "${FILE_PATH}" - | \ + fmt -w "${PREVIEW_WIDTH}" && exit 0 + mutool draw -F txt -i -- "${FILE_PATH}" 1-10 | \ + fmt -w "${PREVIEW_WIDTH}" && exit 0 + exiftool "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## BitTorrent + torrent) + transmission-show -- "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## OpenDocument + odt|ods|odp|sxw) + ## Preview as text conversion + odt2txt "${FILE_PATH}" && exit 0 + ## Preview as markdown conversion + pandoc -s -t markdown -- "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## XLSX + xlsx) + ## Preview as csv conversion + ## Uses: https://github.com/dilshod/xlsx2csv + xlsx2csv -- "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## HTML + htm|html|xhtml) + ## Preview as text conversion + w3m -dump "${FILE_PATH}" && exit 0 + lynx -dump -- "${FILE_PATH}" && exit 0 + elinks -dump "${FILE_PATH}" && exit 0 + pandoc -s -t markdown -- "${FILE_PATH}" && exit 0 + ;; + + ## JSON + json|ipynb) + jq --color-output . "${FILE_PATH}" && exit 0 + python -m json.tool -- "${FILE_PATH}" && exit 0 + ;; + + ## Direct Stream Digital/Transfer (DSDIFF) and wavpack aren't detected + ## by file(1). + dff|dsf|wv|wvc) + mediainfo "${FILE_PATH}" && exit 0 + exiftool "${FILE_PATH}" && exit 0 + ;; # Continue with next handler on failure + esac +} + +handle_mime() { + local mimetype="${1}" + + case "${mimetype}" in + ## RTF and DOC + text/rtf|*msword) + ## Preview as text conversion + ## note: catdoc does not always work for .doc files + ## catdoc: http://www.wagner.pp.ru/~vitus/software/catdoc/ + catdoc -- "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## DOCX, ePub, FB2 (using markdown) + ## You might want to remove "|epub" and/or "|fb2" below if you have + ## uncommented other methods to preview those formats + *wordprocessingml.document|*/epub+zip|*/x-fictionbook+xml) + ## Preview as markdown conversion + pandoc -s -t markdown -- "${FILE_PATH}" | bat -l markdown \ + --color=always --paging=never \ + --style=plain \ + --terminal-width="${PREVIEW_WIDTH}" && exit 0 + exit 1 ;; + + ## E-mails + message/rfc822) + ## Parsing performed by mu: https://github.com/djcb/mu + mu view -- "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## XLS + *ms-excel) + ## Preview as csv conversion + ## xls2csv comes with catdoc: + ## http://www.wagner.pp.ru/~vitus/software/catdoc/ + xls2csv -- "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## Text + text/* | */xml) + bat --color=always --paging=never \ + --style=plain \ + --terminal-width="${PREVIEW_WIDTH}" \ + "${FILE_PATH}" && exit 0 + cat "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## DjVu + image/vnd.djvu) + ## Preview as text conversion (requires djvulibre) + djvutxt "${FILE_PATH}" | fmt -w "${PREVIEW_WIDTH}" && exit 0 + exiftool "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## Image + image/*) + ## Preview as text conversion + exiftool "${FILE_PATH}" && exit 0 + exit 1 ;; + + ## Video and audio + video/* | audio/*) + mediainfo "${FILE_PATH}" && exit 0 + exiftool "${FILE_PATH}" && exit 0 + exit 1 ;; + esac +} + +FILE_EXTENSION="${FILE_PATH##*.}" +FILE_EXTENSION_LOWER="$(printf "%s" "${FILE_EXTENSION}" | tr '[:upper:]' '[:lower:]')" +handle_extension +MIMETYPE="$( file --dereference --brief --mime-type -- "${FILE_PATH}" )" +handle_mime "${MIMETYPE}" + +exit 1 diff --git a/user/ooks/modules/programs/joshuto/config/theme.toml b/user/ooks/modules/programs/joshuto/config/theme.toml new file mode 100644 index 0000000..840d2b0 --- /dev/null +++ b/user/ooks/modules/programs/joshuto/config/theme.toml @@ -0,0 +1,73 @@ +[selection] +fg = "light_yellow" +bold = true + +[visual_mode_selection] +fg = "light_red" +bold = true + +[selection.prefix] +prefix = " " +size = 2 + +[executable] +fg = "light_green" +bold = true + +[regular] +fg = "white" + +[directory] +fg = "light_blue" +bold = true + +[link] +fg = "cyan" +bold = true + +[link_invalid] +fg = "red" +bold = true + +[socket] +fg = "light_magenta" +bold = true + +[ext] + +bmp.fg = "yellow" +gif.fg = "yellow" +heic.fg = "yellow" +jpg.fg = "yellow" +jpeg.fg = "yellow" +pgm.fg = "yellow" +png.fg = "yellow" +ppm.fg = "yellow" +svg.fg = "yellow" + +wav.fg = "magenta" +flac.fg = "magenta" +mp3.fg = "magenta" +amr.fg = "magenta" + +avi.fg = "magenta" +flv.fg = "magenta" +m3u.fg = "magenta" +m4a.fg = "magenta" +m4v.fg = "magenta" +mkv.fg = "magenta" +mov.fg = "magenta" +mp4.fg = "magenta" +mpg.fg = "magenta" +rmvb.fg = "magenta" +webm.fg = "magenta" +wmv.fg = "magenta" + +7z.fg = "red" +bz2.fg = "red" +gz.fg = "red" +rar.fg = "red" +tar.fg = "red" +tgz.fg = "red" +xz.fg = "red" +zip.fg = "red" diff --git a/user/ooks/modules/programs/joshuto/default.nix b/user/ooks/modules/programs/joshuto/default.nix new file mode 100644 index 0000000..e697138 --- /dev/null +++ b/user/ooks/modules/programs/joshuto/default.nix @@ -0,0 +1,10 @@ +{ pkgs, config, ... }: +{ + home = { + packages = with pkgs; [ + file + joshuto + ]; + }; + home.file.".config/joshuto".source = ./config; +} diff --git a/user/ooks/modules/programs/kitty/default.nix b/user/ooks/modules/programs/kitty/default.nix new file mode 100644 index 0000000..275c707 --- /dev/null +++ b/user/ooks/modules/programs/kitty/default.nix @@ -0,0 +1,11 @@ +{ config, pkgs, ... }: + +{ + programs = { + kitty = { + enable = true; + environment = { }; + keybindings = { }; + }; + }; +} diff --git a/user/ooks/modules/programs/lazygit/default.nix b/user/ooks/modules/programs/lazygit/default.nix new file mode 100644 index 0000000..9cc3e2f --- /dev/null +++ b/user/ooks/modules/programs/lazygit/default.nix @@ -0,0 +1,8 @@ +{ config, pkgs, ... }: +{ + home = { + packages = with pkgs; [ + lazygit + ]; + }; +} diff --git a/user/ooks/modules/programs/mpv/default.nix b/user/ooks/modules/programs/mpv/default.nix new file mode 100644 index 0000000..880bbd2 --- /dev/null +++ b/user/ooks/modules/programs/mpv/default.nix @@ -0,0 +1,11 @@ +{ lib, pkgs, user, ... }: + +{ + programs = { + mpv = { + enable = true; + }; + }; + home.file.".config/mpv/mpv.conf".source = ./mpv.conf; + home.file.".config/mpv/scripts/file-browser.lua".source = ./scripts/file-browser.lua; +} diff --git a/user/ooks/modules/programs/mpv/mpv.conf b/user/ooks/modules/programs/mpv/mpv.conf new file mode 100644 index 0000000..283b698 --- /dev/null +++ b/user/ooks/modules/programs/mpv/mpv.conf @@ -0,0 +1,8 @@ +# hwdec=auto +# vo=gpu-next #This will break Anime4K +gpu-api=opengl +gpu-context=wayland +hwdec=auto-safe +vo=gpu +profile=gpu-hq +script-opts=ytdl_hook-ytdl_path=yt-dlp diff --git a/user/ooks/modules/programs/mpv/scripts/file-browser.lua b/user/ooks/modules/programs/mpv/scripts/file-browser.lua new file mode 100644 index 0000000..803a5b6 --- /dev/null +++ b/user/ooks/modules/programs/mpv/scripts/file-browser.lua @@ -0,0 +1,2593 @@ +--[[ + mpv-file-browser + This script allows users to browse and open files and folders entirely from within mpv. + The script uses nothing outside the mpv API, so should work identically on all platforms. + The browser can move up and down directories, start playing files and folders, or add them to the queue. + For full documentation see: https://github.com/CogentRedTester/mpv-file-browser +]] +-- + +local mp = require("mp") +local msg = require("mp.msg") +local utils = require("mp.utils") +local opt = require("mp.options") + +local o = { + --root directories + root = "~/Videos/", + + --characters to use as separators + root_separators = ",;", + + --number of entries to show on the screen at once + num_entries = 20, + + --wrap the cursor around the top and bottom of the list + wrap = false, + + --only show files compatible with mpv + filter_files = true, + + --experimental feature that recurses directories concurrently when + --appending items to the playlist + concurrent_recursion = false, + + --maximum number of recursions that can run concurrently + max_concurrency = 16, + + --enable custom keybinds + custom_keybinds = false, + + --blacklist compatible files, it's recommended to use this rather than to edit the + --compatible list directly. A semicolon separated list of extensions without spaces + extension_blacklist = "", + + --add extra file extensions + extension_whitelist = "", + + --files with these extensions will be added as additional audio tracks for the current file instead of appended to the playlist + audio_extensions = "mka,dts,dtshd,dts-hd,truehd,true-hd", + + --files with these extensions will be added as additional subtitle tracks instead of appended to the playlist + subtitle_extensions = "etf,etf8,utf-8,idx,sub,srt,rt,ssa,ass,mks,vtt,sup,scc,smi,lrc,pgs", + + --filter dot directories like .config + --most useful on linux systems + filter_dot_dirs = false, + filter_dot_files = false, + + --substitude forward slashes for backslashes when appending a local file to the playlist + --potentially useful on windows systems + substitute_backslash = false, + + --this option reverses the behaviour of the alt+ENTER keybind + --when disabled the keybind is required to enable autoload for the file + --when enabled the keybind disables autoload for the file + autoload = false, + + --if autoload is triggered by selecting the currently playing file, then + --the current file will have it's watch-later config saved before being closed + --essentially the current file will not be restarted + autoload_save_current = true, + + --when opening the browser in idle mode prefer the current working directory over the root + --note that the working directory is set as the 'current' directory regardless, so `home` will + --move the browser there even if this option is set to false + default_to_working_directory = false, + + --allows custom icons be set to fix incompatabilities with some fonts + --the `\h` character is a hard space to add padding between the symbol and the text + folder_icon = "🖿", + cursor_icon = "➤", + indent_icon = [[\h\h\h]], + + --enable addons + addons = false, + addon_directory = "~~/script-modules/file-browser-addons", + + --directory to load external modules - currently just user-input-module + module_directory = "~~/script-modules", + + --force file-browser to use a specific text alignment (default: top-left) + --uses ass tag alignment numbers: https://aegi.vmoe.info/docs/3.0/ASS_Tags/#index23h3 + --set to 0 to use the default mpv osd-align options + alignment = 7, + + --style settings + font_bold_header = true, + + font_size_header = 35, + font_size_body = 25, + font_size_wrappers = 16, + + font_name_header = "", + font_name_body = "", + font_name_wrappers = "", + font_name_folder = "", + font_name_cursor = "", + + font_colour_header = "00ccff", + font_colour_body = "ffffff", + font_colour_wrappers = "00ccff", + font_colour_cursor = "00ccff", + + font_colour_multiselect = "fcad88", + font_colour_selected = "fce788", + font_colour_playing = "33ff66", + font_colour_playing_multiselected = "22b547", +} + +opt.read_options(o, "file_browser") +utils.shared_script_property_set("file_browser-open", "no") + +-------------------------------------------------------------------------------------------------------- +-----------------------------------------Environment Setup---------------------------------------------- +-------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------- + +--sets the version for the file-browser API +API_VERSION = "1.3.0" + +--switch the main script to a different environment so that the +--executed lua code cannot access our global variales +if setfenv then + setfenv(1, setmetatable({}, { __index = _G })) +else + _ENV = setmetatable({}, { __index = _G }) +end + +--creates a table for the API functions +--adds one metatable redirect to prevent addon authors from accidentally breaking file-browser +local API = { API_VERSION = API_VERSION } +package.loaded["file-browser"] = setmetatable({}, { __index = API }) + +local parser_API = setmetatable({}, { __index = package.loaded["file-browser"] }) +local parse_state_API = {} + +-------------------------------------------------------------------------------------------------------- +------------------------------------------Variable Setup------------------------------------------------ +-------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------- + +--the osd_overlay API was not added until v0.31. The expand-path command was not added until 0.30 +local ass = mp.create_osd_overlay("ass-events") +if not ass then + return msg.error("Script requires minimum mpv version 0.31") +end + +package.path = mp.command_native({ "expand-path", o.module_directory }) .. "/?.lua;" .. package.path + +local style = { + global = o.alignment == 0 and "" or ([[{\an%d}]]):format(o.alignment), + + -- full line styles + header = ([[{\r\q2\b%s\fs%d\fn%s\c&H%s&}]]):format( + (o.font_bold_header and "1" or "0"), + o.font_size_header, + o.font_name_header, + o.font_colour_header + ), + body = ([[{\r\q2\fs%d\fn%s\c&H%s&}]]):format(o.font_size_body, o.font_name_body, o.font_colour_body), + footer_header = ([[{\r\q2\fs%d\fn%s\c&H%s&}]]):format( + o.font_size_wrappers, + o.font_name_wrappers, + o.font_colour_wrappers + ), + + --small section styles (for colours) + multiselect = ([[{\c&H%s&}]]):format(o.font_colour_multiselect), + selected = ([[{\c&H%s&}]]):format(o.font_colour_selected), + playing = ([[{\c&H%s&}]]):format(o.font_colour_playing), + playing_selected = ([[{\c&H%s&}]]):format(o.font_colour_playing_multiselected), + + --icon styles + cursor = ([[{\fn%s\c&H%s&}]]):format(o.font_name_cursor, o.font_colour_cursor), + folder = ([[{\fn%s}]]):format(o.font_name_folder), +} + +local state = { + list = {}, + selected = 1, + hidden = true, + flag_update = false, + keybinds = nil, + + parser = nil, + directory = nil, + directory_label = nil, + prev_directory = "", + co = nil, + + multiselect_start = nil, + initial_selection = nil, + selection = {}, +} + +--the parser table actually contains 3 entries for each parser +--a numeric entry which represents the priority of the parsers and has the parser object as the value +--a string entry representing the id of each parser and with the parser object as the value +--and a table entry with the parser itself as the key and a table value in the form { id = %s, index = %d } +local parsers = {} + +--this table contains the parse_state tables for every parse operation indexed with the coroutine used for the parse +--this table has weakly referenced keys, meaning that once the coroutine for a parse is no-longer used by anything that +--field in the table will be removed by the garbage collector +local parse_states = setmetatable({}, { __mode = "k" }) + +local extensions = {} +local sub_extensions = {} +local audio_extensions = {} +local parseable_extensions = {} + +local dvd_device = nil +local current_file = { + directory = nil, + name = nil, + path = nil, +} + +local root = nil + +--default list of compatible file extensions +--adding an item to this list is a valid request on github +local compatible_file_extensions = { + "264", + "265", + "3g2", + "3ga", + "3ga2", + "3gp", + "3gp2", + "3gpp", + "3iv", + "a52", + "aac", + "adt", + "adts", + "ahn", + "aif", + "aifc", + "aiff", + "amr", + "ape", + "asf", + "au", + "avc", + "avi", + "awb", + "ay", + "bmp", + "cue", + "divx", + "dts", + "dtshd", + "dts-hd", + "dv", + "dvr", + "dvr-ms", + "eac3", + "evo", + "evob", + "f4a", + "flac", + "flc", + "fli", + "flic", + "flv", + "gbs", + "gif", + "gxf", + "gym", + "h264", + "h265", + "hdmov", + "hdv", + "hes", + "hevc", + "jpeg", + "jpg", + "kss", + "lpcm", + "m1a", + "m1v", + "m2a", + "m2t", + "m2ts", + "m2v", + "m3u", + "m3u8", + "m4a", + "m4v", + "mk3d", + "mka", + "mkv", + "mlp", + "mod", + "mov", + "mp1", + "mp2", + "mp2v", + "mp3", + "mp4", + "mp4v", + "mp4v", + "mpa", + "mpe", + "mpeg", + "mpeg2", + "mpeg4", + "mpg", + "mpg4", + "mpv", + "mpv2", + "mts", + "mtv", + "mxf", + "nsf", + "nsfe", + "nsv", + "nut", + "oga", + "ogg", + "ogm", + "ogv", + "ogx", + "opus", + "pcm", + "pls", + "png", + "qt", + "ra", + "ram", + "rm", + "rmvb", + "sap", + "snd", + "spc", + "spx", + "svg", + "thd", + "thd+ac3", + "tif", + "tiff", + "tod", + "trp", + "truehd", + "true-hd", + "ts", + "tsa", + "tsv", + "tta", + "tts", + "vfw", + "vgm", + "vgz", + "vob", + "vro", + "wav", + "weba", + "webm", + "webp", + "wm", + "wma", + "wmv", + "wtv", + "wv", + "x264", + "x265", + "xvid", + "y4m", + "yuv", +} + +-------------------------------------------------------------------------------------------------------- +--------------------------------------Cache Implementation---------------------------------------------- +-------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------- + +--metatable of methods to manage the cache +local __cache = {} + +__cache.cached_values = { + "directory", + "directory_label", + "list", + "selected", + "selection", + "parser", + "empty_text", + "co", +} + +--inserts latest state values onto the cache stack +function __cache:push() + local t = {} + for _, value in ipairs(self.cached_values) do + t[value] = state[value] + end + table.insert(self, t) +end + +function __cache:pop() + table.remove(self) +end + +function __cache:apply() + local t = self[#self] + for _, value in ipairs(self.cached_values) do + state[value] = t[value] + end +end + +function __cache:clear() + for i = 1, #self do + self[i] = nil + end +end + +local cache = setmetatable({}, { __index = __cache }) + +-------------------------------------------------------------------------------------------------------- +-----------------------------------------Utility Functions---------------------------------------------- +---------------------------------------Part of the addon API-------------------------------------------- +-------------------------------------------------------------------------------------------------------- + +API.coroutine = {} +local ABORT_ERROR = { + msg = "browser is no longer waiting for list - aborting parse", +} + +--implements table.pack if on lua 5.1 +if not table.pack then + table.unpack = unpack + function table.pack(...) + local t = { ... } + t.n = select("#", ...) + return t + end +end + +--prints an error message and a stack trace +--accepts an error object and optionally a coroutine +--can be passed directly to xpcall +function API.traceback(errmsg, co) + if co then + msg.warn(debug.traceback(co)) + else + msg.warn(debug.traceback("", 2)) + end + msg.error(errmsg) +end + +--prints an error if a coroutine returns an error +--unlike the next function this one still returns the results of coroutine.resume() +function API.coroutine.resume_catch(...) + local returns = table.pack(coroutine.resume(...)) + if not returns[1] and returns[2] ~= ABORT_ERROR then + API.traceback(returns[2], select(1, ...)) + end + return table.unpack(returns, 1, returns.n) +end + +--resumes a coroutine and prints an error if it was not sucessful +function API.coroutine.resume_err(...) + local success, err = coroutine.resume(...) + if not success and err ~= ABORT_ERROR then + API.traceback(err, select(1, ...)) + end + return success +end + +--in lua 5.1 there is only one return value which will be nil if run from the main thread +--in lua 5.2 main will be true if running from the main thread +function API.coroutine.assert(err) + local co, main = coroutine.running() + assert(not main and co, err or "error - function must be executed from within a coroutine") + return co +end + +--creates a callback fuction to resume the current coroutine +function API.coroutine.callback() + local co = API.coroutine.assert("cannot create a coroutine callback for the main thread") + return function(...) + return API.coroutine.resume_err(co, ...) + end +end + +--puts the current coroutine to sleep for the given number of seconds +function API.coroutine.sleep(n) + mp.add_timeout(n, API.coroutine.callback()) + coroutine.yield() +end + +--runs the given function in a coroutine, passing through any additional arguments +--this is for triggering an event in a coroutine +function API.coroutine.run(fn, ...) + local co = coroutine.create(fn) + API.coroutine.resume_err(co, ...) +end + +--get the full path for the current file +function API.get_full_path(item, dir) + if item.path then + return item.path + end + return (dir or state.directory) .. item.name +end + +--gets the path for a new subdirectory, redirects if the path field is set +--returns the new directory path and a boolean specifying if a redirect happened +function API.get_new_directory(item, directory) + if item.path and item.redirect ~= false then + return item.path, true + end + if directory == "" then + return item.name + end + if string.sub(directory, -1) == "/" then + return directory .. item.name + end + return directory .. "/" .. item.name +end + +--returns the file extension of the given file +function API.get_extension(filename, def) + return string.lower(filename):match("%.([^%./]+)$") or def +end + +--returns the protocol scheme of the given url, or nil if there is none +function API.get_protocol(filename, def) + return string.lower(filename):match("^(%a[%w+-.]*)://") or def +end + +--formats strings for ass handling +--this function is based on a similar function from https://github.com/mpv-player/mpv/blob/master/player/lua/console.lua#L110 +function API.ass_escape(str, replace_newline) + if replace_newline == true then + replace_newline = "\\\239\187\191n" + end + + --escape the invalid single characters + str = string.gsub(str, "[\\{}\n]", { + -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if + -- it isn't followed by a recognised character, so add a zero-width + -- non-breaking space + ["\\"] = "\\\239\187\191", + ["{"] = "\\{", + ["}"] = "\\}", + -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of + -- consecutive newlines + ["\n"] = "\239\187\191\\N", + }) + + -- Turn leading spaces into hard spaces to prevent ASS from stripping them + str = str:gsub("\\N ", "\\N\\h") + str = str:gsub("^ ", "\\h") + + if replace_newline then + str = str:gsub("\\N", replace_newline) + end + return str +end + +--escape lua pattern characters +function API.pattern_escape(str) + return string.gsub(str, "([%^%$%(%)%%%.%[%]%*%+%-])", "%%%1") +end + +--standardises filepaths across systems +function API.fix_path(str, is_directory) + str = string.gsub(str, [[\]], [[/]]) + str = str:gsub([[/./]], [[/]]) + if is_directory and str:sub(-1) ~= "/" then + str = str .. "/" + end + return str +end + +--wrapper for utils.join_path to handle protocols +function API.join_path(working, relative) + return API.get_protocol(relative) and relative or utils.join_path(working, relative) +end + +--sorts the table lexicographically ignoring case and accounting for leading/non-leading zeroes +--the number format functionality was proposed by github user twophyro, and was presumably taken +--from here: http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua +function API.sort(t) + local function padnum(d) + local r = string.match(d, "0*(.+)") + return ("%03d%s"):format(#r, r) + end + + --appends the letter d or f to the start of the comparison to sort directories and folders as well + table.sort(t, function(a, b) + return a.type:sub(1, 1) .. (a.label or a.name):lower():gsub("%d+", padnum) + < b.type:sub(1, 1) .. (b.label or b.name):lower():gsub("%d+", padnum) + end) + return t +end + +function API.valid_dir(dir) + if o.filter_dot_dirs and string.sub(dir, 1, 1) == "." then + return false + end + return true +end + +function API.valid_file(file) + if o.filter_dot_files and (string.sub(file, 1, 1) == ".") then + return false + end + if o.filter_files and not extensions[API.get_extension(file, "")] then + return false + end + return true +end + +--returns whether or not the item can be parsed +function API.parseable_item(item) + return item.type == "dir" or parseable_extensions[API.get_extension(item.name, "")] +end + +--removes items and folders from the list +--this is for addons which can't filter things during their normal processing +function API.filter(t) + local max = #t + local top = 1 + for i = 1, max do + local temp = t[i] + t[i] = nil + + if + (temp.type == "dir" and API.valid_dir(temp.label or temp.name)) + or (temp.type == "file" and API.valid_file(temp.label or temp.name)) + then + t[top] = temp + top = top + 1 + end + end + return t +end + +--returns a string iterator that uses the root separators +function API.iterate_opt(str) + return string.gmatch(str, "([^" .. API.pattern_escape(o.root_separators) .. "]+)") +end + +--sorts a table into an array of selected items in the correct order +--if a predicate function is passed, then the item will only be added to +--the table if the function returns true +function API.sort_keys(t, include_item) + local keys = {} + for k in pairs(t) do + local item = state.list[k] + if not include_item or include_item(item) then + item.index = k + keys[#keys + 1] = item + end + end + + table.sort(keys, function(a, b) + return a.index < b.index + end) + return keys +end + +local invalid_types = { + userdata = true, + thread = true, + ["function"] = true, +} + +local invalid_key_types = { + boolean = true, + table = true, + ["nil"] = true, +} +setmetatable(invalid_key_types, { __index = invalid_types }) + +--recursively removes elements of the table which would cause +--utils.format_json to throw an error +local function json_safe_recursive(t) + if type(t) ~= "table" then + return t + end + + local invalid_ktypes = setmetatable({}, { __index = invalid_key_types }) + local arr_length = #t + if arr_length > 0 then + invalid_ktypes.string = true + setmetatable(t, { type = "ARRAY" }) + else + invalid_ktypes.number = true + setmetatable(t, { type = "MAP" }) + end + + for key, value in pairs(t) do + local ktype = type(key) + local vtype = type(value) + + if invalid_ktypes[ktype] or invalid_types[vtype] then + t[key] = nil + elseif ktype == "number" and key > arr_length then + t[key] = nil + else + t[key] = json_safe_recursive(t[key]) + end + end + return t +end + +--formats a table into a json string but ensures there are no invalid datatypes inside the table first +function API.format_json_safe(t) + --operate on a copy of the table to prevent any data loss in the original table + t = json_safe_recursive(API.copy_table(t)) + local success, result, err = pcall(utils.format_json, t) + if success then + return result, err + else + return nil, result + end +end + +--copies a table without leaving any references to the original +--uses a structured clone algorithm to maintain cyclic references +local function copy_table_recursive(t, references) + if type(t) ~= "table" then + return t + end + if references[t] then + return references[t] + end + + local mt = { + __original = t, + __index = getmetatable(t), + } + local copy = setmetatable({}, mt) + references[t] = copy + + for key, value in pairs(t) do + key = copy_table_recursive(key, references) + copy[key] = copy_table_recursive(value, references) + end + return copy +end + +--a wrapper around copy_table to provide the reference table +function API.copy_table(t) + --this is to handle cyclic table references + return copy_table_recursive(t, {}) +end + +-------------------------------------------------------------------------------------------------------- +------------------------------------Parser Object Implementation---------------------------------------- +-------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------- + +--parser object for the root +--this object is not added to the parsers table so that scripts cannot get access to +--the root table, which is returned directly by parse() +local root_parser = { + name = "root", + priority = math.huge, + + --if this is being called then all other parsers have failed and we've fallen back to root + can_parse = function() + return true + end, + + --we return the root directory exactly as setup + parse = function(self) + return root, { + sorted = true, + filtered = true, + escaped = true, + parser = self, + directory = "", + } + end, +} + +--parser ofject for native filesystems +local file_parser = { + name = "file", + priority = 110, + + --as the default parser we'll always attempt to use it if all others fail + can_parse = function(_, directory) + return true + end, + + --scans the given directory using the mp.utils.readdir function + parse = function(self, directory) + local new_list = {} + local list1 = utils.readdir(directory, "dirs") + if list1 == nil then + return nil + end + + --sorts folders and formats them into the list of directories + for i = 1, #list1 do + local item = list1[i] + + --filters hidden dot directories for linux + if self.valid_dir(item) then + msg.trace(item .. "/") + table.insert(new_list, { name = item .. "/", type = "dir" }) + end + end + + --appends files to the list of directory items + local list2 = utils.readdir(directory, "files") + for i = 1, #list2 do + local item = list2[i] + + --only adds whitelisted files to the browser + if self.valid_file(item) then + msg.trace(item) + table.insert(new_list, { name = item, type = "file" }) + end + end + return API.sort(new_list), { filtered = true, sorted = true } + end, +} + +-------------------------------------------------------------------------------------------------------- +-----------------------------------------List Formatting------------------------------------------------ +-------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------- + +--appends the entered text to the overlay +local function append(text) + if text == nil then + return + end + ass.data = ass.data .. text +end + +--appends a newline character to the osd +local function newline() + ass.data = ass.data .. "\\N" +end + +--detects whether or not to highlight the given entry as being played +local function highlight_entry(v) + if current_file.name == nil then + return false + end + if API.parseable_item(v) then + return current_file.directory:find(API.get_full_path(v), 1, true) + else + return current_file.path == API.get_full_path(v) + end +end + +--saves the directory and name of the currently playing file +local function update_current_directory(_, filepath) + --if we're in idle mode then we want to open the working directory + if filepath == nil then + current_file.directory = API.fix_path(mp.get_property("working-directory", ""), true) + current_file.name = nil + current_file.path = nil + return + elseif filepath:find("dvd://") == 1 then + filepath = dvd_device .. filepath:match("dvd://(.*)") + end + + local workingDirectory = mp.get_property("working-directory", "") + local exact_path = API.join_path(workingDirectory, filepath) + exact_path = API.fix_path(exact_path, false) + current_file.directory, current_file.name = utils.split_path(exact_path) + current_file.path = exact_path +end + +--refreshes the ass text using the contents of the list +local function update_ass() + if state.hidden then + state.flag_update = true + return + end + + ass.data = style.global + + local dir_name = state.directory_label or state.directory + if dir_name == "" then + dir_name = "ROOT" + end + append(style.header) + append(API.ass_escape(dir_name, style.cursor .. "\\\239\187\191n" .. style.header)) + append("\\N ----------------------------------------------------") + newline() + + if #state.list < 1 then + append(state.empty_text) + ass:update() + return + end + + local start = 1 + local finish = start + o.num_entries - 1 + + --handling cursor positioning + local mid = math.ceil(o.num_entries / 2) + 1 + if state.selected + mid > finish then + local offset = state.selected - finish + mid + + --if we've overshot the end of the list then undo some of the offset + if finish + offset > #state.list then + offset = offset - ((finish + offset) - #state.list) + end + + start = start + offset + finish = finish + offset + end + + --making sure that we don't overstep the boundaries + if start < 1 then + start = 1 + end + local overflow = finish < #state.list + --this is necessary when the number of items in the dir is less than the max + if not overflow then + finish = #state.list + end + + --adding a header to show there are items above in the list + if start > 1 then + append(style.footer_header .. (start - 1) .. " item(s) above\\N\\N") + end + + for i = start, finish do + local v = state.list[i] + local playing_file = highlight_entry(v) + append(style.body) + + --handles custom styles for different entries + if i == state.selected then + append(style.cursor) + append((state.multiselect_start and style.multiselect or "") .. o.cursor_icon) + append("\\h" .. style.body) + else + append(o.indent_icon .. "\\h" .. style.body) + end + + --sets the selection colour scheme + local multiselected = state.selection[i] + if multiselected then + append(style.multiselect) + elseif i == state.selected then + append(style.selected) + end + + --prints the currently-playing icon and style + if playing_file and multiselected then + append(style.playing_selected) + elseif playing_file then + append(style.playing) + end + + --sets the folder icon + if v.type == "dir" then + append(style.folder .. o.folder_icon .. "\\h" .. "{\\fn" .. o.font_name_body .. "}") + end + + --adds the actual name of the item + append(v.ass or API.ass_escape(v.label or v.name, true)) + newline() + end + + if overflow then + append("\\N" .. style.footer_header .. #state.list - finish .. " item(s) remaining") + end + ass:update() +end + +-------------------------------------------------------------------------------------------------------- +--------------------------------Scroll/Select Implementation-------------------------------------------- +-------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------- + +--disables multiselect +local function disable_select_mode() + state.multiselect_start = nil + state.initial_selection = nil +end + +--enables multiselect +local function enable_select_mode() + state.multiselect_start = state.selected + state.initial_selection = API.copy_table(state.selection) +end + +--calculates what drag behaviour is required for that specific movement +local function drag_select(original_pos, new_pos) + if original_pos == new_pos then + return + end + + local setting = state.selection[state.multiselect_start] + for i = original_pos, new_pos, (new_pos > original_pos and 1 or -1) do + --if we're moving the cursor away from the starting point then set the selection + --otherwise restore the original selection + if i > state.multiselect_start then + if new_pos > original_pos then + state.selection[i] = setting + elseif i ~= new_pos then + state.selection[i] = state.initial_selection[i] + end + elseif i < state.multiselect_start then + if new_pos < original_pos then + state.selection[i] = setting + elseif i ~= new_pos then + state.selection[i] = state.initial_selection[i] + end + end + end +end + +--moves the selector up and down the list by the entered amount +local function scroll(n, wrap) + local num_items = #state.list + if num_items == 0 then + return + end + + local original_pos = state.selected + + if original_pos + n > num_items then + state.selected = wrap and 1 or num_items + elseif original_pos + n < 1 then + state.selected = wrap and num_items or 1 + else + state.selected = original_pos + n + end + + if state.multiselect_start then + drag_select(original_pos, state.selected) + end + update_ass() +end + +--toggles the selection +local function toggle_selection() + if not state.list[state.selected] then + return + end + state.selection[state.selected] = not state.selection[state.selected] or nil + update_ass() +end + +--select all items in the list +local function select_all() + for i, _ in ipairs(state.list) do + state.selection[i] = true + end + update_ass() +end + +--toggles select mode +local function toggle_select_mode() + if state.multiselect_start == nil then + enable_select_mode() + toggle_selection() + else + disable_select_mode() + update_ass() + end +end + +-------------------------------------------------------------------------------------------------------- +-----------------------------------------Directory Movement--------------------------------------------- +-------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------- + +--scans the list for which item to select by default +--chooses the folder that the script just moved out of +--or, otherwise, the item highlighted as currently playing +local function select_prev_directory() + if state.prev_directory:find(state.directory, 1, true) == 1 then + local i = 1 + while state.list[i] and API.parseable_item(state.list[i]) do + if state.prev_directory:find(API.get_full_path(state.list[i]), 1, true) then + state.selected = i + return + end + i = i + 1 + end + end + + for i, item in ipairs(state.list) do + if highlight_entry(item) then + state.selected = i + return + end + end +end + +--parses the given directory or defers to the next parser if nil is returned +local function choose_and_parse(directory, index) + msg.debug("finding parser for", directory) + local parser, list, opts + local parse_state = API.get_parse_state() + while list == nil and not parse_state.already_deferred and index <= #parsers do + parser = parsers[index] + if parser:can_parse(directory, parse_state) then + msg.debug("attempting parser:", parser:get_id()) + list, opts = parser:parse(directory, parse_state) + end + index = index + 1 + end + if not list then + return nil, {} + end + + msg.debug("list returned from:", parser:get_id()) + opts = opts or {} + if list then + opts.id = opts.id or parser:get_id() + end + return list, opts +end + +--sets up the parse_state table and runs the parse operation +local function run_parse(directory, parse_state) + msg.verbose("scanning files in", directory) + parse_state.directory = directory + local co = coroutine.running() + + setmetatable(parse_state, { __index = parse_state_API }) + if directory == "" then + return root_parser:parse() + end + + parse_states[co] = parse_state + local list, opts = choose_and_parse(directory, 1) + + if list == nil then + return msg.debug("no successful parsers found") + end + opts.parser = parsers[opts.id] + + if not opts.filtered then + API.filter(list) + end + if not opts.sorted then + API.sort(list) + end + return list, opts +end + +--returns the contents of the given directory using the given parse state +--if a coroutine has already been used for a parse then create a new coroutine so that +--the every parse operation has a unique thread ID +local function parse_directory(directory, parse_state) + local co = API.coroutine.assert( + "scan_directory must be executed from within a coroutine - aborting scan " .. utils.to_string(parse_state) + ) + if not parse_states[co] then + return run_parse(directory, parse_state) + end + + --if this coroutine is already is use by another parse operation then we create a new + --one and hand execution over to that + local new_co = coroutine.create(function() + API.coroutine.resume_err(co, run_parse(directory, parse_state)) + end) + + --queue the new coroutine on the mpv event queue + mp.add_timeout(0, function() + local success, err = coroutine.resume(new_co) + if not success then + API.traceback(err, new_co) + API.coroutine.resume_err(co) + end + end) + return parse_states[co]:yield() +end + +--sends update requests to the different parsers +local function update_list() + msg.verbose("opening directory: " .. state.directory) + + state.selected = 1 + state.selection = {} + + --loads the current directry from the cache to save loading time + --there will be a way to forcibly reload the current directory at some point + --the cache is in the form of a stack, items are taken off the stack when the dir moves up + if cache[1] and cache[#cache].directory == state.directory then + msg.verbose("found directory in cache") + cache:apply() + state.prev_directory = state.directory + return + end + local directory = state.directory + local list, opts = parse_directory(state.directory, { source = "browser" }) + + --if the running coroutine isn't the one stored in the state variable, then the user + --changed directories while the coroutine was paused, and this operation should be aborted + if coroutine.running() ~= state.co then + msg.verbose(ABORT_ERROR.msg) + msg.debug("expected:", state.directory, "received:", directory) + return + end + + --apply fallbacks if the scan failed + if not list and cache[1] then + --switches settings back to the previously opened directory + --to the user it will be like the directory never changed + msg.warn("could not read directory", state.directory) + cache:apply() + return + elseif not list then + msg.warn("could not read directory", state.directory) + list, opts = root_parser:parse() + end + + state.list = list + state.parser = opts.parser + + --this only matters when displaying the list on the screen, so it doesn't need to be in the scan function + if not opts.escaped then + for i = 1, #list do + list[i].ass = list[i].ass or API.ass_escape(list[i].label or list[i].name, true) + end + end + + --setting custom options from parsers + state.directory_label = opts.directory_label + state.empty_text = opts.empty_text or state.empty_text + + --we assume that directory is only changed when redirecting to a different location + --therefore, the cache should be wiped + if opts.directory then + state.directory = opts.directory + cache:clear() + end + + if opts.selected_index then + state.selected = opts.selected_index or state.selected + if state.selected > #state.list then + state.selected = #state.list + elseif state.selected < 1 then + state.selected = 1 + end + end + + select_prev_directory() + state.prev_directory = state.directory +end + +--rescans the folder and updates the list +local function update(moving_adjacent) + --we can only make assumptions about the directory label when moving from adjacent directories + if not moving_adjacent then + state.directory_label = nil + cache:clear() + end + + state.empty_text = "~" + state.list = {} + disable_select_mode() + update_ass() + state.empty_text = "empty directory" + + --the directory is always handled within a coroutine to allow addons to + --pause execution for asynchronous operations + state.co = coroutine.create(function() + update_list() + update_ass() + end) + API.coroutine.resume_err(state.co) +end + +--the base function for moving to a directory +local function goto_directory(directory) + state.directory = directory + update() +end + +--loads the root list +local function goto_root() + msg.verbose("jumping to root") + goto_directory("") +end + +--switches to the directory of the currently playing file +local function goto_current_dir() + msg.verbose("jumping to current directory") + goto_directory(current_file.directory) +end + +--moves up a directory +local function up_dir() + local dir = state.directory:reverse() + local index = dir:find("[/\\]") + + while index == 1 do + dir = dir:sub(2) + index = dir:find("[/\\]") + end + + if index == nil then + state.directory = "" + else + state.directory = dir:sub(index):reverse() + end + + --we can make some assumptions about the next directory label when moving up or down + if state.directory_label then + state.directory_label = state.directory_label:match("^(.+/)[^/]+/$") + end + + update(true) + cache:pop() +end + +--moves down a directory +local function down_dir() + local current = state.list[state.selected] + if not current or not API.parseable_item(current) then + return + end + + cache:push() + local directory, redirected = API.get_new_directory(current, state.directory) + state.directory = directory + + --we can make some assumptions about the next directory label when moving up or down + if state.directory_label then + state.directory_label = state.directory_label .. (current.label or current.name) + end + update(not redirected) +end + +------------------------------------------------------------------------------------------ +------------------------------------Browser Controls-------------------------------------- +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ + +--opens the browser +local function open() + for _, v in ipairs(state.keybinds) do + mp.add_forced_key_binding(v[1], "dynamic/" .. v[2], v[3], v[4]) + end + + utils.shared_script_property_set("file_browser-open", "yes") + state.hidden = false + if state.directory == nil then + local path = mp.get_property("path") + update_current_directory(nil, path) + if path or o.default_to_working_directory then + goto_current_dir() + else + goto_root() + end + return + end + + if state.flag_update then + update_current_directory(nil, mp.get_property("path")) + end + if not state.flag_update then + ass:update() + else + state.flag_update = false + update_ass() + end +end + +--closes the list and sets the hidden flag +local function close() + for _, v in ipairs(state.keybinds) do + mp.remove_key_binding("dynamic/" .. v[2]) + end + + utils.shared_script_property_set("file_browser-open", "no") + state.hidden = true + ass:remove() +end + +--toggles the list +local function toggle() + if state.hidden then + open() + else + close() + end +end + +--run when the escape key is used +local function escape() + --if multiple items are selection cancel the + --selection instead of closing the browser + if next(state.selection) or state.multiselect_start then + state.selection = {} + disable_select_mode() + update_ass() + return + end + close() +end + +--opens a specific directory +local function browse_directory(directory) + if not directory then + return + end + directory = mp.command_native({ "expand-path", directory }, "") + -- directory = join_path( mp.get_property("working-directory", ""), directory ) + + if directory ~= "" then + directory = API.fix_path(directory, true) + end + msg.verbose("recieved directory from script message: " .. directory) + + if directory == "dvd://" then + directory = dvd_device + end + goto_directory(directory) + open() +end + +------------------------------------------------------------------------------------------ +---------------------------------File/Playlist Opening------------------------------------ +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ + +--adds a file to the playlist and changes the flag to `append-play` in preparation +--for future items +local function loadfile(file, opts) + if o.substitute_backslash and not API.get_protocol(file) then + file = file:gsub("/", "\\") + end + + if opts.flag == "replace" then + msg.verbose("Playling file", file) + else + msg.verbose("Appending", file, "to the playlist") + end + + if not mp.commandv("loadfile", file, opts.flag) then + msg.warn(file) + end + opts.flag = "append-play" + opts.items_appended = opts.items_appended + 1 +end + +--this function recursively loads directories concurrently in separate coroutines +--results are saved in a tree of tables that allows asynchronous access +local function concurrent_loadlist_parse(directory, load_opts, prev_dirs, item_t) + --prevents infinite recursion from the item.path or opts.directory fields + if prev_dirs[directory] then + return + end + prev_dirs[directory] = true + + local list, list_opts = parse_directory(directory, { source = "loadlist" }) + if list == root then + return + end + + --if we can't parse the directory then append it and hope mpv fares better + if list == nil then + msg.warn("Could not parse", directory, "appending to playlist anyway") + item_t.type = "file" + return + end + + directory = list_opts.directory or directory + if directory == "" then + return + end + + --we must declare these before we start loading sublists otherwise the append thread will + --need to wait until the whole list is loaded (when synchronous IO is used) + item_t._sublist = list or {} + list._directory = directory + + --launches new parse operations for directories, each in a different coroutine + for _, item in ipairs(list) do + if API.parseable_item(item) then + API.coroutine.run( + concurrent_loadlist_wrapper, + API.get_new_directory(item, directory), + load_opts, + prev_dirs, + item + ) + end + end + return true +end + +--a wrapper function that ensures the concurrent_loadlist_parse is run correctly +function concurrent_loadlist_wrapper(directory, opts, prev_dirs, item) + --ensures that only a set number of concurrent parses are operating at any one time. + --the mpv event queue is seemingly limited to 1000 items, but only async mpv actions like + --command_native_async should use that, events like mp.add_timeout (which coroutine.sleep() uses) should + --be handled enturely on the Lua side with a table, which has a significantly larger maximum size. + while opts.concurrency > o.max_concurrency do + API.coroutine.sleep(0.1) + end + opts.concurrency = opts.concurrency + 1 + + local success = concurrent_loadlist_parse(directory, opts, prev_dirs, item) + opts.concurrency = opts.concurrency - 1 + if not success then + item._sublist = {} + end + if coroutine.status(opts.co) == "suspended" then + API.coroutine.resume_err(opts.co) + end +end + +--recursively appends items to the playlist, acts as a consumer to the previous functions producer; +--if the next directory has not been parsed this function will yield until the parse has completed +local function concurrent_loadlist_append(list, load_opts) + local directory = list._directory + + for _, item in ipairs(list) do + if + not sub_extensions[API.get_extension(item.name, "")] + and not audio_extensions[API.get_extension(item.name, "")] + then + while not item._sublist and API.parseable_item(item) do + coroutine.yield() + end + + if API.parseable_item(item) then + concurrent_loadlist_append(item._sublist, load_opts) + else + loadfile(API.get_full_path(item, directory), load_opts) + end + end + end +end + +--recursive function to load directories using the script custom parsers +--returns true if any items were appended to the playlist +local function custom_loadlist_recursive(directory, load_opts, prev_dirs) + --prevents infinite recursion from the item.path or opts.directory fields + if prev_dirs[directory] then + return + end + prev_dirs[directory] = true + + local list, opts = parse_directory(directory, { source = "loadlist" }) + if list == root then + return + end + + --if we can't parse the directory then append it and hope mpv fares better + if list == nil then + msg.warn("Could not parse", directory, "appending to playlist anyway") + loadfile(directory, load_opts.flag) + return true + end + + directory = opts.directory or directory + if directory == "" then + return + end + + for _, item in ipairs(list) do + if + not sub_extensions[API.get_extension(item.name, "")] + and not audio_extensions[API.get_extension(item.name, "")] + then + if API.parseable_item(item) then + custom_loadlist_recursive(API.get_new_directory(item, directory), load_opts, prev_dirs) + else + local path = API.get_full_path(item, directory) + loadfile(path, load_opts) + end + end + end +end + +--a wrapper for the custom_loadlist_recursive function +local function loadlist(item, opts) + local dir = API.get_full_path(item, opts.directory) + local num_items = opts.items_appended + + if o.concurrent_recursion then + item = API.copy_table(item) + opts.co = API.coroutine.assert() + opts.concurrency = 0 + + --we need the current coroutine to suspend before we run the first parse operation, so + --we schedule the coroutine to run on the mpv event queue + mp.add_timeout(0, function() + API.coroutine.run(concurrent_loadlist_wrapper, dir, opts, {}, item) + end) + concurrent_loadlist_append({ item, _directory = opts.directory }, opts) + else + custom_loadlist_recursive(dir, opts, {}) + end + + if opts.items_appended == num_items then + msg.warn(dir, "contained no valid files") + end +end + +--load playlist entries before and after the currently playing file +local function autoload_dir(path, opts) + if o.autoload_save_current and path == current_file.path then + mp.commandv("write-watch-later-config") + end + + --loads the currently selected file, clearing the playlist in the process + loadfile(path, opts) + + local pos = 1 + local file_count = 0 + for _, item in ipairs(state.list) do + if + item.type == "file" + and not sub_extensions[API.get_extension(item.name, "")] + and not audio_extensions[API.get_extension(item.name, "")] + then + local p = API.get_full_path(item) + + if p == path then + pos = file_count + else + loadfile(p, opts) + end + + file_count = file_count + 1 + end + end + mp.commandv("playlist-move", 0, pos + 1) +end + +--runs the loadfile or loadlist command +local function open_item(item, opts) + if API.parseable_item(item) then + return loadlist(item, opts) + end + + local path = API.get_full_path(item, opts.directory) + if sub_extensions[API.get_extension(item.name, "")] then + mp.commandv("sub-add", path, opts.flag == "replace" and "select" or "auto") + elseif audio_extensions[API.get_extension(item.name, "")] then + mp.commandv("audio-add", path, opts.flag == "replace" and "select" or "auto") + else + if opts.autoload then + autoload_dir(path, opts) + else + loadfile(path, opts) + end + end +end + +--handles the open options as a coroutine +--once loadfile has been run we can no-longer guarantee synchronous execution - the state values may change +--therefore, we must ensure that any state values that could be used after a loadfile call are saved beforehand +local function open_file_coroutine(opts) + if not state.list[state.selected] then + return + end + if opts.flag == "replace" then + close() + end + + --we want to set the idle option to yes to ensure that if the first item + --fails to load then the player has a chance to attempt to load further items (for async append operations) + local idle = mp.get_property("idle", "once") + mp.set_property("idle", "yes") + + --handles multi-selection behaviour + if next(state.selection) then + local selection = API.sort_keys(state.selection) + --reset the selection after + state.selection = {} + + disable_select_mode() + update_ass() + + --the currently selected file will be loaded according to the flag + --the flag variable will be switched to append once a file is loaded + for i = 1, #selection do + open_item(selection[i], opts) + end + else + local item = state.list[state.selected] + if opts.flag == "replace" then + down_dir() + end + open_item(item, opts) + end + + if mp.get_property("idle") == "yes" then + mp.set_property("idle", idle) + end +end + +--opens the selelected file(s) +local function open_file(flag, autoload) + API.coroutine.run(open_file_coroutine, { + flag = flag, + autoload = (autoload ~= o.autoload and flag == "replace"), + directory = state.directory, + items_appended = 0, + }) +end + +------------------------------------------------------------------------------------------ +----------------------------------Keybind Implementation---------------------------------- +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ + +state.keybinds = { + { + "ENTER", + "play", + function() + open_file("replace", false) + end, + }, + { + "Shift+ENTER", + "play_append", + function() + open_file("append-play", false) + end, + }, + { + "Alt+ENTER", + "play_autoload", + function() + open_file("replace", true) + end, + }, + { "ESC", "close", escape }, + { "RIGHT", "down_dir", down_dir }, + { "LEFT", "up_dir", up_dir }, + { + "DOWN", + "scroll_down", + function() + scroll(1, o.wrap) + end, + { repeatable = true }, + }, + { + "UP", + "scroll_up", + function() + scroll(-1, o.wrap) + end, + { repeatable = true }, + }, + { + "PGDWN", + "page_down", + function() + scroll(o.num_entries) + end, + { repeatable = true }, + }, + { + "PGUP", + "page_up", + function() + scroll(-o.num_entries) + end, + { repeatable = true }, + }, + { + "Shift+PGDWN", + "list_bottom", + function() + scroll(math.huge) + end, + }, + { + "Shift+PGUP", + "list_top", + function() + scroll(-math.huge) + end, + }, + { "HOME", "goto_current", goto_current_dir }, + { "Shift+HOME", "goto_root", goto_root }, + { + "Ctrl+r", + "reload", + function() + cache:clear() + update() + end, + }, + { "s", "select_mode", toggle_select_mode }, + { "S", "select_item", toggle_selection }, + { "Ctrl+a", "select_all", select_all }, +} + +--characters used for custom keybind codes +local CUSTOM_KEYBIND_CODES = "%fFnNpPdDrR" + +--a map of key-keybinds - only saves the latest keybind if multiple have the same key code +local top_level_keys = {} + +--format the item string for either single or multiple items +local function create_item_string(cmd, items, funct) + if not items[1] then + return + end + + local str = funct(items[1]) + for i = 2, #items do + str = str .. (cmd["concat-string"] or " ") .. funct(items[i]) + end + return str +end + +--iterates through the command table and substitutes special +--character codes for the correct strings used for custom functions +local function format_command_table(cmd, items, state) + local copy = {} + for i = 1, #cmd.command do + copy[i] = {} + + for j = 1, #cmd.command[i] do + copy[i][j] = cmd.command[i][j]:gsub("%%[" .. CUSTOM_KEYBIND_CODES .. "]", { + ["%%"] = "%", + ["%f"] = create_item_string(cmd, items, function(item) + return item and API.get_full_path(item, state.directory) or "" + end), + ["%F"] = create_item_string(cmd, items, function(item) + return string.format("%q", item and API.get_full_path(item, state.directory) or "") + end), + ["%n"] = create_item_string(cmd, items, function(item) + return item and (item.label or item.name) or "" + end), + ["%N"] = create_item_string(cmd, items, function(item) + return string.format("%q", item and (item.label or item.name) or "") + end), + ["%p"] = state.directory or "", + ["%P"] = string.format("%q", state.directory or ""), + ["%d"] = (state.directory_label or state.directory):match("([^/]+)/?$") or "", + ["%D"] = string.format("%q", (state.directory_label or state.directory):match("([^/]+)/$") or ""), + ["%r"] = state.parser.keybind_name or state.parser.name or "", + ["%R"] = string.format("%q", state.parser.keybind_name or state.parser.name or ""), + }) + end + end + return copy +end + +--runs all of the commands in the command table +--key.command must be an array of command tables compatible with mp.command_native +--items must be an array of multiple items (when multi-type ~= concat the array will be 1 long) +local function run_custom_command(cmd, items, state) + local custom_cmds = cmd.codes and format_command_table(cmd, items, state) or cmd.command + + for _, cmd in ipairs(custom_cmds) do + msg.debug("running command:", utils.to_string(cmd)) + mp.command_native(cmd) + end +end + +--runs one of the custom commands +local function custom_command(cmd, state, co) + if cmd.parser and cmd.parser ~= (state.parser.keybind_name or state.parser.name) then + return false + end + + --the function terminates here if we are running the command on a single item + if not (cmd.multiselect and next(state.selection)) then + if cmd.filter then + if not state.list[state.selected] then + return false + end + if state.list[state.selected].type ~= cmd.filter then + return false + end + end + + --if the directory is empty, and this command needs to work on an item, then abort and fallback to the next command + if cmd.codes and not state.list[state.selected] then + if cmd.codes["%f"] or cmd.codes["%F"] or cmd.codes["%n"] or cmd.codes["%N"] then + return false + end + end + + run_custom_command(cmd, { state.list[state.selected] }, state) + return true + end + + --runs the command on all multi-selected items + local selection = API.sort_keys(state.selection, function(item) + return not cmd.filter or item.type == cmd.filter + end) + if not next(selection) then + return false + end + + if cmd["multi-type"] == "concat" then + run_custom_command(cmd, selection, state) + elseif cmd["multi-type"] == "repeat" then + for i, _ in ipairs(selection) do + run_custom_command(cmd, { selection[i] }, state) + + if cmd.delay then + mp.add_timeout(cmd.delay, function() + API.coroutine.resume_err(co) + end) + coroutine.yield() + end + end + end + + --we passthrough by default if the command is not run on every selected item + if cmd.passthrough ~= nil then + return + end + + local num_selection = 0 + for _ in pairs(state.selection) do + num_selection = num_selection + 1 + end + return #selection == num_selection +end + +--recursively runs the keybind functions, passing down through the chain +--of keybinds with the same key value +local function run_keybind_recursive(keybind, state, co) + msg.trace("Attempting custom command:", utils.to_string(keybind)) + + --these are for the default keybinds, or from addons which use direct functions + local addon_fn = type(keybind.command) == "function" + local fn = addon_fn and keybind.command or custom_command + + if keybind.passthrough ~= nil then + fn(keybind, addon_fn and API.copy_table(state) or state, co) + if keybind.passthrough == true and keybind.prev_key then + run_keybind_recursive(keybind.prev_key, state, co) + end + else + if fn(keybind, state, co) == false and keybind.prev_key then + run_keybind_recursive(keybind.prev_key, state, co) + end + end +end + +--a wrapper to run a custom keybind as a lua coroutine +local function run_keybind_coroutine(key) + msg.debug("Received custom keybind " .. key.key) + local co = coroutine.create(run_keybind_recursive) + + local state_copy = { + directory = state.directory, + directory_label = state.directory_label, + list = state.list, --the list should remain unchanged once it has been saved to the global state, new directories get new tables + selected = state.selected, + selection = API.copy_table(state.selection), + parser = state.parser, + } + local success, err = coroutine.resume(co, key, state_copy, co) + if not success then + msg.error("error running keybind:", utils.to_string(key)) + API.traceback(err, co) + end +end + +--scans the given command table to identify if they contain any custom keybind codes +local function scan_for_codes(command_table, codes) + if type(command_table) ~= "table" then + return codes + end + for _, value in pairs(command_table) do + local type = type(value) + if type == "table" then + scan_for_codes(value, codes) + elseif type == "string" then + value:gsub("%%[" .. CUSTOM_KEYBIND_CODES .. "]", function(code) + codes[code] = true + end) + end + end + return codes +end + +--inserting the custom keybind into the keybind array for declaration when file-browser is opened +--custom keybinds with matching names will overwrite eachother +local function insert_custom_keybind(keybind) + --we'll always save the keybinds as either an array of command arrays or a function + if type(keybind.command) == "table" and type(keybind.command[1]) ~= "table" then + keybind.command = { keybind.command } + end + + keybind.codes = scan_for_codes(keybind.command, {}) + if not next(keybind.codes) then + keybind.codes = nil + end + keybind.prev_key = top_level_keys[keybind.key] + + table.insert(state.keybinds, { + keybind.key, + keybind.name, + function() + run_keybind_coroutine(keybind) + end, + keybind.flags or {}, + }) + top_level_keys[keybind.key] = keybind +end + +--loading the custom keybinds +--can either load keybinds from the config file, from addons, or from both +local function setup_keybinds() + if not o.custom_keybinds and not o.addons then + return + end + + --this is to make the default keybinds compatible with passthrough from custom keybinds + for _, keybind in ipairs(state.keybinds) do + top_level_keys[keybind[1]] = { key = keybind[1], name = keybind[2], command = keybind[3], flags = keybind[4] } + end + + --this loads keybinds from addons + if o.addons then + for i = #parsers, 1, -1 do + local parser = parsers[i] + if parser.keybinds then + for i, keybind in ipairs(parser.keybinds) do + --if addons use the native array command format, then we need to convert them over to the custom command format + if not keybind.key then + keybind = { key = keybind[1], name = keybind[2], command = keybind[3], flags = keybind[4] } + else + keybind = API.copy_table(keybind) + end + + keybind.name = parsers[parser].id .. "/" .. (keybind.name or tostring(i)) + insert_custom_keybind(keybind) + end + end + end + end + + --loads custom keybinds from file-browser-keybinds.json + if o.custom_keybinds then + local path = mp.command_native({ "expand-path", "~~/script-opts" }) .. "/file-browser-keybinds.json" + local custom_keybinds, err = io.open(path) + if not custom_keybinds then + return error(err) + end + + local json = custom_keybinds:read("*a") + custom_keybinds:close() + + json = utils.parse_json(json) + if not json then + return error("invalid json syntax for " .. path) + end + + for i, keybind in ipairs(json) do + keybind.name = "custom/" .. (keybind.name or tostring(i)) + insert_custom_keybind(keybind) + end + end +end + +-------------------------------------------------------------------------------------------------------- +-------------------------------------------API Functions------------------------------------------------ +-------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------- + +--these functions we'll provide as-is +API.redraw = update_ass +API.rescan = update +API.browse_directory = browse_directory + +function API.clear_cache() + cache:clear() +end + +--a wrapper around scan_directory for addon API +function API.parse_directory(directory, parse_state) + if not parse_state then + parse_state = { source = "addon" } + elseif not parse_state.source then + parse_state.source = "addon" + end + return parse_directory(directory, parse_state) +end + +--register file extensions which can be opened by the browser +function API.register_parseable_extension(ext) + parseable_extensions[string.lower(ext)] = true +end +function API.remove_parseable_extension(ext) + parseable_extensions[string.lower(ext)] = nil +end + +--add a compatible extension to show through the filter, only applies if run during the setup() method +function API.add_default_extension(ext) + table.insert(compatible_file_extensions, ext) +end + +--add item to root at position pos +function API.insert_root_item(item, pos) + msg.verbose("adding item to root", item.label or item.name) + item.ass = item.ass or API.ass_escape(item.label or item.name) + item.type = "dir" + table.insert(root, pos or (#root + 1), item) +end + +--providing getter and setter functions so that addons can't modify things directly +function API.get_script_opts() + return API.copy_table(o) +end +function API.get_opt(key) + return o[key] +end +function API.get_extensions() + return API.copy_table(extensions) +end +function API.get_sub_extensions() + return API.copy_table(sub_extensions) +end +function API.get_audio_extensions() + return API.copy_table(audio_extensions) +end +function API.get_parseable_extensions() + return API.copy_table(parseable_extensions) +end +function API.get_state() + return API.copy_table(state) +end +function API.get_dvd_device() + return dvd_device +end +function API.get_parsers() + return API.copy_table(parsers) +end +function API.get_root() + return API.copy_table(root) +end +function API.get_directory() + return state.directory +end +function API.get_list() + return API.copy_table(state.list) +end +function API.get_current_file() + return API.copy_table(current_file) +end +function API.get_current_parser() + return state.parser:get_id() +end +function API.get_current_parser_keyname() + return state.parser.keybind_name or state.parser.name +end +function API.get_selected_index() + return state.selected +end +function API.get_selected_item() + return API.copy_table(state.list[state.selected]) +end +function API.get_open_status() + return not state.hidden +end +function API.get_parse_state(co) + return parse_states[co or coroutine.running() or ""] +end + +function API.set_empty_text(str) + state.empty_text = str + API.redraw() +end + +function API.set_selected_index(index) + if type(index) ~= "number" then + return false + end + if index < 1 then + index = 1 + end + if index > #state.list then + index = #state.list + end + state.selected = index + API.redraw() + return index +end + +function parser_API:get_index() + return parsers[self].index +end +function parser_API:get_id() + return parsers[self].id +end + +--runs choose_and_parse starting from the next parser +function parser_API:defer(directory) + msg.trace("deferring to other parsers...") + local list, opts = choose_and_parse(directory, self:get_index() + 1) + API.get_parse_state().already_deferred = true + return list, opts +end + +--a wrapper around coroutine.yield that aborts the coroutine if +--the parse request was cancelled by the user +--the coroutine is +function parse_state_API:yield(...) + local co = coroutine.running() + local is_browser = co == state.co + if self.source == "browser" and not is_browser then + msg.error("current coroutine does not match browser's expected coroutine - did you unsafely yield before this?") + error("current coroutine does not match browser's expected coroutine - aborting the parse") + end + + local result = table.pack(coroutine.yield(...)) + if is_browser and co ~= state.co then + msg.verbose("browser no longer waiting for list - aborting parse for", self.directory) + error(ABORT_ERROR) + end + return unpack(result, 1, result.n) +end + +--checks if the current coroutine is the one handling the browser's request +function parse_state_API:is_coroutine_current() + return coroutine.running() == state.co +end + +-------------------------------------------------------------------------------------------------------- +-----------------------------------------Setup Functions------------------------------------------------ +-------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------- + +local API_MAJOR, API_MINOR, API_PATCH = API_VERSION:match("(%d+)%.(%d+)%.(%d+)") + +--checks if the given parser has a valid version number +local function check_api_version(parser) + local version = parser.version or "1.0.0" + + local major, minor = version:match("(%d+)%.(%d+)") + + if not major or not minor then + return msg.error("Invalid version number") + elseif major ~= API_MAJOR then + return msg.error( + "parser", + parser.name, + "has wrong major version number, expected", + ("v%d.x.x"):format(API_MAJOR), + "got", + "v" .. version + ) + elseif minor > API_MINOR then + msg.warn( + "parser", + parser.name, + "has newer minor version number than API, expected", + ("v%d.%d.x"):format(API_MAJOR, API_MINOR), + "got", + "v" .. version + ) + end + return true +end + +--create a unique id for the given parser +local function set_parser_id(parser) + local name = parser.name + if parsers[name] then + local n = 2 + name = parser.name .. "_" .. n + while parsers[name] do + n = n + 1 + name = parser.name .. "_" .. n + end + end + + parsers[name] = parser + parsers[parser] = { id = name } +end + +local function redirect_table(t) + return setmetatable({}, { __index = t }) +end + +--loads an addon in a separate environment +local function load_addon(path) + local name_sqbr = string.format("[%s]", path:match("/([^/]*)%.lua$")) + local addon_environment = redirect_table(_G) + addon_environment._G = addon_environment + + --gives each addon custom debug messages + addon_environment.package = redirect_table(addon_environment.package) + addon_environment.package.loaded = redirect_table(addon_environment.package.loaded) + local msg_module = { + log = function(level, ...) + msg.log(level, name_sqbr, ...) + end, + fatal = function(...) + return msg.fatal(name_sqbr, ...) + end, + error = function(...) + return msg.error(name_sqbr, ...) + end, + warn = function(...) + return msg.warn(name_sqbr, ...) + end, + info = function(...) + return msg.info(name_sqbr, ...) + end, + verbose = function(...) + return msg.verbose(name_sqbr, ...) + end, + debug = function(...) + return msg.debug(name_sqbr, ...) + end, + trace = function(...) + return msg.trace(name_sqbr, ...) + end, + } + addon_environment.print = msg_module.info + + addon_environment.require = function(module) + if module == "mp.msg" then + return msg_module + end + return require(module) + end + + local chunk, err + if setfenv then + --since I stupidly named a function loadfile I need to specify the global one + --I've been using the name too long to want to change it now + chunk, err = _G.loadfile(path) + if not chunk then + return msg.error(err) + end + setfenv(chunk, addon_environment) + else + chunk, err = _G.loadfile(path, "bt", addon_environment) + if not chunk then + return msg.error(err) + end + end + + local success, result = xpcall(chunk, API.traceback) + return success and result or nil +end + +--setup an internal or external parser +local function setup_parser(parser, file) + parser = setmetatable(parser, { __index = parser_API }) + parser.name = parser.name or file:gsub("%-browser%.lua$", ""):gsub("%.lua$", "") + + set_parser_id(parser) + if not check_api_version(parser) then + return msg.error("aborting load of parser", parser:get_id(), "from", file) + end + + msg.verbose("imported parser", parser:get_id(), "from", file) + + --sets missing functions + if not parser.can_parse then + if parser.parse then + parser.can_parse = function() + return true + end + else + parser.can_parse = function() + return false + end + end + end + + if parser.priority == nil then + parser.priority = 0 + end + if type(parser.priority) ~= "number" then + return msg.error("parser", parser:get_id(), "needs a numeric priority") + end + + table.insert(parsers, parser) +end + +--load an external addon +local function setup_addon(file, path) + if file:sub(-4) ~= ".lua" then + return msg.verbose(path, "is not a lua file - aborting addon setup") + end + + local addon_parsers = load_addon(path) + if not addon_parsers or type(addon_parsers) ~= "table" then + return msg.error("addon", path, "did not return a table") + end + + --if the table contains a priority key then we assume it isn't an array of parsers + if not addon_parsers[1] then + addon_parsers = { addon_parsers } + end + + for _, parser in ipairs(addon_parsers) do + setup_parser(parser, file) + end +end + +--loading external addons +local function setup_addons() + local addon_dir = mp.command_native({ "expand-path", o.addon_directory .. "/" }) + local files = utils.readdir(addon_dir) + if not files then + error("could not read addon directory") + end + + for _, file in ipairs(files) do + setup_addon(file, addon_dir .. file) + end + table.sort(parsers, function(a, b) + return a.priority < b.priority + end) + + --we want to store the indexes of the parsers + for i = #parsers, 1, -1 do + parsers[parsers[i]].index = i + end + + --we want to run the setup functions for each addon + for index, parser in ipairs(parsers) do + if parser.setup then + local success = xpcall(function() + parser:setup() + end, API.traceback) + if not success then + msg.error( + "parser", + parser:get_id(), + "threw an error in the setup method - removing from list of parsers" + ) + table.remove(parsers, index) + end + end + end +end + +--sets up the compatible extensions list +local function setup_extensions_list() + --setting up subtitle extensions + for ext in API.iterate_opt(o.subtitle_extensions:lower()) do + sub_extensions[ext] = true + extensions[ext] = true + end + + --setting up audio extensions + for ext in API.iterate_opt(o.audio_extensions:lower()) do + audio_extensions[ext] = true + extensions[ext] = true + end + + --adding file extensions to the set + for _, ext in ipairs(compatible_file_extensions) do + extensions[ext] = true + end + + --adding extra extensions on the whitelist + for str in API.iterate_opt(o.extension_whitelist:lower()) do + extensions[str] = true + end + + --removing extensions that are in the blacklist + for str in API.iterate_opt(o.extension_blacklist:lower()) do + extensions[str] = nil + end +end + +--splits the string into a table on the semicolons +local function setup_root() + root = {} + for str in API.iterate_opt(o.root) do + local path = mp.command_native({ "expand-path", str }) + path = API.fix_path(path, true) + + local temp = { name = path, type = "dir", label = str, ass = API.ass_escape(str, true) } + + root[#root + 1] = temp + end +end + +setup_root() + +setup_parser(file_parser, "file-browser.lua") +if o.addons then + --all of the API functions need to be defined before this point for the addons to be able to access them safely + setup_addons() +end + +--these need to be below the addon setup in case any parsers add custom entries +setup_extensions_list() +setup_keybinds() + +------------------------------------------------------------------------------------------ +------------------------------Other Script Compatability---------------------------------- +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ + +local function scan_directory_json(directory, response_str) + if not directory then + msg.error("did not receive a directory string") + return + end + if not response_str then + msg.error("did not receive a response string") + return + end + + directory = mp.command_native({ "expand-path", directory }, "") + if directory ~= "" then + directory = API.fix_path(directory, true) + end + msg.verbose( + ("recieved %q from 'get-directory-contents' script message - returning result to %q"):format( + directory, + response_str + ) + ) + + local list, opts = parse_directory(directory, { source = "script-message" }) + opts.API_VERSION = API_VERSION + + local err + list, err = API.format_json_safe(list) + if not list then + msg.error(err) + end + + opts, err = API.format_json_safe(opts) + if not opts then + msg.error(err) + end + + mp.commandv("script-message", response_str, list or "", opts or "") +end + +pcall(function() + local input = require("user-input-module") + mp.add_key_binding("Alt+o", "browse-directory/get-user-input", function() + input.get_user_input(browse_directory, { request_text = "open directory:" }) + end) +end) + +------------------------------------------------------------------------------------------ +--------------------------------mpv API Callbacks----------------------------------------- +------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------ + +--we don't want to add any overhead when the browser isn't open +mp.observe_property("path", "string", function(_, path) + if not state.hidden then + update_current_directory(_, path) + update_ass() + else + state.flag_update = true + end +end) + +--updates the dvd_device +mp.observe_property("dvd-device", "string", function(_, device) + if not device or device == "" then + device = "/dev/dvd/" + end + dvd_device = API.fix_path(device, true) +end) + +--declares the keybind to open the browser +mp.add_key_binding("MENU", "browse-files", toggle) +mp.add_key_binding("Ctrl+o", "open-browser", open) + +--allows keybinds/other scripts to auto-open specific directories +mp.register_script_message("browse-directory", browse_directory) + +--allows other scripts to request directory contents from file-browser +mp.register_script_message("get-directory-contents", function(directory, response_str) + API.coroutine.run(scan_directory_json, directory, response_str) +end) diff --git a/user/ooks/modules/programs/neofetch/default.nix b/user/ooks/modules/programs/neofetch/default.nix new file mode 100644 index 0000000..a521339 --- /dev/null +++ b/user/ooks/modules/programs/neofetch/default.nix @@ -0,0 +1,5 @@ +{ config, lib, pkgs, ... }: + +{ + home.packages = [ pkgs.neofetch ]; +} diff --git a/user/ooks/modules/programs/notify/default.nix b/user/ooks/modules/programs/notify/default.nix new file mode 100644 index 0000000..2f98e3b --- /dev/null +++ b/user/ooks/modules/programs/notify/default.nix @@ -0,0 +1,6 @@ +{ config, pkgs, ... }: +{ + services.mako = { + enable = true; + }; +} diff --git a/user/ooks/modules/programs/resource-monitor/default.nix b/user/ooks/modules/programs/resource-monitor/default.nix new file mode 100644 index 0000000..ffb4f9e --- /dev/null +++ b/user/ooks/modules/programs/resource-monitor/default.nix @@ -0,0 +1,8 @@ +{ config, pkgs, ... }: +{ + programs = { + btop = { + enable = true; + }; + }; +} diff --git a/user/ooks/modules/programs/search/default.nix b/user/ooks/modules/programs/search/default.nix new file mode 100644 index 0000000..36a85d8 --- /dev/null +++ b/user/ooks/modules/programs/search/default.nix @@ -0,0 +1,14 @@ +{ config, pkgs, ... }: + +{ + home = { + packages = with pkgs; [ + fd + bat + ripgrep + ]; + }; + programs = { + fzf.enable = true; + }; +} diff --git a/user/ooks/modules/programs/starship/default.nix b/user/ooks/modules/programs/starship/default.nix new file mode 100644 index 0000000..a23709d --- /dev/null +++ b/user/ooks/modules/programs/starship/default.nix @@ -0,0 +1,15 @@ +{ config, pkgs, ... }: + +{ + programs = { + starship = { + enable = true; + setting = { + character = { + success_symbol = "[➜](bold green)"; + error_symbol = "[➜](bold red)"; + } + } + }; + }; +} diff --git a/user/ooks/modules/programs/youtube-tui/default.nix b/user/ooks/modules/programs/youtube-tui/default.nix new file mode 100644 index 0000000..542debd --- /dev/null +++ b/user/ooks/modules/programs/youtube-tui/default.nix @@ -0,0 +1,8 @@ +{ config, pkgs, ... }: +{ + home = { + packages = with pkgs; [ + youtube-tui + ]; + }; +} diff --git a/user/ooks/modules/programs/yt-dlp/default.nix b/user/ooks/modules/programs/yt-dlp/default.nix new file mode 100644 index 0000000..cb3f19e --- /dev/null +++ b/user/ooks/modules/programs/yt-dlp/default.nix @@ -0,0 +1,17 @@ +{ config, pkgs, ... }: + +{ + programs = { + yt-dlp = { + enable = true; + }; + }; + home.file = { + ".config/yt-dlp/config".text = '' + --ignore-errors + -o ~/Videos/%(title)s.%(ext)s + # Prefer 1080p or lower resolutions + -f bestvideo[ext=mp4][width<2000][height<=1200]+bestaudio[ext=m4a]/bestvideo[ext=webm][width<2000][height<=1200]+bestaudio[ext=webm]/bestvideo[width<2000][height<=1200]+bestaudio/best[width<2000][height<=1200]/best + ''; + }; +} diff --git a/user/ooks/modules/programs/zathura/default.nix b/user/ooks/modules/programs/zathura/default.nix new file mode 100644 index 0000000..378b904 --- /dev/null +++ b/user/ooks/modules/programs/zathura/default.nix @@ -0,0 +1,76 @@ +{ lib, pkgs, user, ... }: + +{ + programs.zathura = { + enable = true; + extraConfig = '' + # Zathura configuration file + # See man `man zathurarc' + + # Open document in fit-width mode by default + set adjust-open "best-fit" + + # One page per row by default + set pages-per-row 1 + + #stop at page boundries + set scroll-page-aware "true" + set scroll-full-overlap 0.01 + set scroll-step 100 + + #zoom settings + set zoom-min 10 + set guioptions "" + + # zathurarc-dark + + set font "JetBrains Mono Nerd Font 15" + set default-fg "#96CDFB" + set default-bg "#1A1823" + + set completion-bg "#1A1823" + set completion-fg "#96cdfb" + set completion-highlight-bg "#302D41" + set completion-highlight-fg "#96cdfb" + set completion-group-bg "#1a1823" + set completion-group-fg "#89DCEB" + + set statusbar-fg "#C9CBFF" + set statusbar-bg "#1A1823" + set statusbar-h-padding 10 + set statusbar-v-padding 10 + + set notification-bg "#1A1823" + set notification-fg "#D9E0EE" + set notification-error-bg "#d9e0ee" + set notification-error-fg "#D9E0EE" + set notification-warning-bg "#FAE3B0" + set notification-warning-fg "#D9E0EE" + set selection-notification "true" + + set inputbar-fg "#C9CBFF" + set inputbar-bg "#1A1823" + + set recolor "true" + set recolor-lightcolor "#D9E0EE" + set recolor-darkcolor "#1A1823" + + set index-fg "#96cdfb" + set index-bg "#1A1823" + set index-active-fg "#96cdfb" + set index-active-bg "#1A1823" + + set render-loading-bg "#1A1823" + set render-loading-fg "#96cdfb" + + set highlight-color "#96cdfb" + set highlight-active-color "#DDB6F2" + + + set render-loading "false" + set scroll-step 50 + + set selection-clipboard clipboard + ''; + }; +} diff --git a/walls/everforest/megacity.png b/walls/everforest/megacity.png new file mode 100644 index 0000000..de5f5f8 Binary files /dev/null and b/walls/everforest/megacity.png differ