commit 66a4a699b01c126cf735333dfec1694c94f8e5b3 Author: Osaigbovo Omere Date: Sat Aug 15 19:39:06 2026 +0100 initialise: dotfiles diff --git a/.config/README.md b/.config/README.md new file mode 100644 index 0000000..da96052 --- /dev/null +++ b/.config/README.md @@ -0,0 +1,35 @@ +# .config + +This directory contains user-specific configuration files conforming to the XDG Base Directory Specification. The configurations map out a modular, Wayland-centric desktop environment alongside core terminal and system utilities. + +## COMPOSITORS AND SESSION MANAGEMENT +* **niri:** Scrollable-tiling Wayland compositor. +* **sway:** Tiling Wayland compositor and X11/i3 replacement. +* **swaylock:** Screen locking utility for Wayland sessions. +* **nwg-displays:** Output and display management. + +## TERMINALS AND SHELL ENVIRONMENT +* **kitty:** GPU-accelerated terminal emulator. +* **foot:** Lightweight Wayland terminal emulator. +* **fish:** Interactive shell configuration. +* **tmux:** Terminal multiplexer for persistent workspace sessions. +* **starship.toml:** Cross-shell prompt configuration. + +## SYSTEM UI AND THEMING +* **waybar:** Customisable Wayland status bar. +* **fuzzel:** Application launcher for wlroots-based compositors. +* **mako:** Lightweight notification daemon. +* **nwg-look:** GTK settings editor for maintaining visual consistency. +* **qt5ct / qt6ct:** Qt configuration utilities for uniform theming across toolkits. + +## MEDIA AND DOCUMENTS +* **mpd:** Music Player Daemon for backend audio management. +* **ncmpcpp:** Feature-rich ncurses client for MPD. +* **mpv:** Hardware-accelerated command-line media player. +* **zathura:** Keyboard-driven document and PDF viewer. + +## SYSTEM UTILITIES AND DEVELOPMENT +* **nvim:** Neovim configuration for development and text editing. +* **git:** Global version control parameters. +* **btop:** Resource monitor for system usage and hardware polling. +* **incus:** Local configuration for the container and virtual machine daemon. diff --git a/.config/alacritty/alacritty.toml b/.config/alacritty/alacritty.toml new file mode 100644 index 0000000..f18700f --- /dev/null +++ b/.config/alacritty/alacritty.toml @@ -0,0 +1,63 @@ +# --- CORE CAPABILITIES --- +[env] +TERM = "alacritty" + +[window] +padding = { x = 8, y = 8 } +dynamic_padding = true +# sway manages window borders and aesthetics +decorations = "none" +opacity = 1.0 + +[scrolling] +history = 10000 +multiplier = 3 + +# --- TYPOGRAPHY --- +[font] +size = 9.0 + +[font.normal] +family = "JetBrainsMono Nerd Font" +style = "Regular" + +[font.bold] +family = "JetBrainsMono Nerd Font" +style = "Bold" + +[font.italic] +family = "JetBrainsMono Nerd Font" +style = "Italic" + +# --- blackK --- +[colors.primary] +background = "#030303" +foreground = "#eeeeee" + +[colors.cursor] +text = "#100f0f" +cursor = "#cecdc3" + +[colors.normal] +black = "#100f0f" +red = "#af3029" +green = "#66800b" +yellow = "#ad8301" +blue = "#205ea6" +magenta = "#5e409d" +cyan = "#24837b" +white = "#cecdc3" + +[colors.bright] +black = "#282726" +red = "#d14d41" +green = "#879a39" +yellow = "#d0a215" +blue = "#4385be" +magenta = "#8b7ec8" +cyan = "#3aa99f" +white = "#e6e4d9" + +# --- KEYBOARD AND MOUSE --- +[mouse] +hide_when_typing = true diff --git a/.config/btop/btop.conf b/.config/btop/btop.conf new file mode 100644 index 0000000..31fbf0b --- /dev/null +++ b/.config/btop/btop.conf @@ -0,0 +1,286 @@ +#? Config file for btop v.1.4.7 + +#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. +#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" +color_theme = "/home/dil/.config/btop/themes/flexoki-dark.theme" + +#* If the theme set background should be shown, set to False if you want terminal background transparency. +theme_background = true + +#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. +truecolor = true + +#* Set to true to force tty mode regardless if a real tty has been detected or not. +#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols. +force_tty = false + +#* Option to disable presets. Either the default preset, custom presets, or all presets. +#* "Off" All presets are enabled. +#* "Default" preset is disabled.#* "Custom" presets are disabled.#* "All" presets are disabled. +disable_presets = "Off" + +#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets. +#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. +#* Use whitespace " " as separator between different presets. +#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty" +presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty" + +#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists. +#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. +vim_keys = false + +#* Disable all mouse events. +disable_mouse = false + +#* Rounded corners on boxes, is ignored if TTY mode is ON. +rounded_corners = true + +#* Use terminal synchronized output sequences to reduce flickering on supported terminals. +terminal_sync = true + +#* Default symbols to use for graph creation, "braille", "block" or "tty". +#* "braille" offers the highest resolution but might not be included in all fonts. +#* "block" has half the resolution of braille but uses more common characters. +#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY. +#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view. +graph_symbol = "braille" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_cpu = "default" + +# Graph symbol to use for graphs in gpu box, "default", "braille", "block" or "tty". +graph_symbol_gpu = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_mem = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_net = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_proc = "default" + +#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace. +shown_boxes = "cpu proc" + +#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs. +update_ms = 2000 + +#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct", +#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly. +proc_sorting = "cpu lazy" + +#* Reverse sorting order, True or False. +proc_reversed = false + +#* Show processes as a tree. +proc_tree = false + +#* Use the cpu graph colors in the process list. +proc_colors = true + +#* Use a darkening gradient in the process list. +proc_gradient = true + +#* If process cpu usage should be of the core it's running on or usage of the total available cpu power. +proc_per_core = false + +#* Show process memory as bytes instead of percent. +proc_mem_bytes = true + +#* Show cpu graph for each process. +proc_cpu_graphs = true + +#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate) +proc_info_smaps = false + +#* Show proc box on left side of screen instead of right. +proc_left = false + +#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop). +proc_filter_kernel = false + +#* Should the process list follow the selected process when detailed view is open. +proc_follow_detailed = true + +#* In tree-view, always accumulate child process resources in the parent process. +proc_aggregate = false + +#* Should cpu and memory usage display be preserved for dead processes when paused. +keep_dead_proc_usage = false + +#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_upper = "Auto" + +#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_lower = "Auto" + +#* If gpu info should be shown in the cpu box. Available values = "Auto", "On" and "Off". +show_gpu_info = "Auto" + +#* Toggles if the lower CPU graph should be inverted. +cpu_invert_lower = true + +#* Set to True to completely disable the lower CPU graph. +cpu_single_graph = false + +#* Show cpu box at bottom of screen instead of top. +cpu_bottom = false + +#* Shows the system uptime in the CPU box. +show_uptime = true + +#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo. +show_cpu_watts = true + +#* Show cpu temperature. +check_temp = true + +#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors. +cpu_sensor = "Auto" + +#* Show temperatures for cpu cores also if check_temp is True and sensors has been found. +show_coretemp = true + +#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core. +#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine. +#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries. +#* Example: "4:0 5:1 6:3" +cpu_core_map = "" + +#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine". +temp_scale = "celsius" + +#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024. +base_10_sizes = false + +#* Show CPU frequency. +show_cpu_freq = true + +#* How to calculate CPU frequency, available values: "first", "range", "lowest", "highest" and "average". +freq_mode = "first" + +#* Draw a clock at top of screen, formatting according to strftime, empty string to disable. +#* Special formatting: /host = hostname | /user = username | /uptime = system uptime +clock_format = "%X" + +#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort. +background_update = true + +#* Custom cpu model name, empty string to disable. +custom_cpu_name = "" + +#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ". +#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disk_filter="/boot /home/user", disks_filter="exclude=/boot /home/user" +disks_filter = "" + +#* Show graphs instead of meters for memory values. +mem_graphs = true + +#* Show mem box below net box instead of above. +mem_below_net = false + +#* Count ZFS ARC in cached and available memory. +zfs_arc_cached = true + +#* If swap memory should be shown in memory box. +show_swap = true + +#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk. +swap_disk = true + +#* If mem box should be split to also show disks info. +show_disks = true + +#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar. +only_physical = true + +#* Read disks list from /etc/fstab. This also disables only_physical. +use_fstab = true + +#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool) +zfs_hide_datasets = false + +#* Set to true to show available disk space for privileged users. +disk_free_priv = false + +#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view. +show_io_stat = true + +#* Toggles io mode for disks, showing big graphs for disk read/write speeds. +io_mode = false + +#* Set to True to show combined read/write io graphs in io mode. +io_graph_combined = false + +#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ". +#* Example: "/mnt/media:100 /:20 /boot:1". +io_graph_speeds = "" + +#* Swap the positions of the upload and download speed graphs. When true, upload will be on top. +swap_upload_download = false + +#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False. +net_download = 100 + +net_upload = 100 + +#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest. +net_auto = true + +#* Sync the auto scaling for download and upload to whichever currently has the highest scale. +net_sync = true + +#* Starts with the Network Interface specified here. +net_iface = "" + +#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes. +base_10_bitrate = "Auto" + +#* Show battery stats in top right if battery is present. +show_battery = true + +#* Which battery to use if multiple are present. "Auto" for auto detection. +selected_battery = "Auto" + +#* Show power stats of battery next to charge indicator. +show_battery_watts = true + +#* Set loglevel for "~/.local/state/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG". +#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. +log_level = "WARNING" + +#* Automatically save current settings to config file on exit. +save_config_on_exit = true + +#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards. +nvml_measure_pcie_speeds = true + +#* Measure PCIe throughput on AMD cards, may impact performance on certain cards. +rsmi_measure_pcie_speeds = true + +#* Horizontally mirror the GPU graph. +gpu_mirror_graph = true + +#* Set which GPU vendors to show. Available values are "nvidia amd intel apple" +shown_gpus = "nvidia amd intel" + +#* Custom gpu0 model name, empty string to disable. +custom_gpu_name0 = "" + +#* Custom gpu1 model name, empty string to disable. +custom_gpu_name1 = "" + +#* Custom gpu2 model name, empty string to disable. +custom_gpu_name2 = "" + +#* Custom gpu3 model name, empty string to disable. +custom_gpu_name3 = "" + +#* Custom gpu4 model name, empty string to disable. +custom_gpu_name4 = "" + +#* Custom gpu5 model name, empty string to disable. +custom_gpu_name5 = "" diff --git a/.config/fuzzel/fuzzel.ini b/.config/fuzzel/fuzzel.ini new file mode 100644 index 0000000..5a3a6e3 --- /dev/null +++ b/.config/fuzzel/fuzzel.ini @@ -0,0 +1,27 @@ +# THE NOIR THEME + +[main] +# fuzzel requires wrapper scripts for dynamic per-mode prompts +prompt="APP: " +font=JetBrainsMono Nerd Font:size=8,Noto Color Emoji:size=8 +# character width dictates the horizontal size +width=50 +lines=8 +icons-enabled=yes +terminal=footclient +# disable the generic exit button to maintain the sharp aesthetic +exit-on-keyboard-focus-loss=yes + +[colors] +# flexoki ink translations +background=100f0fff +text=cecdc3ff +match=8b7ec8ff +selection=282726ff +selection-text=cecdc3ff +selection-match=8b7ec8ff +border=343331ff + +[border] +width=1 +radius=0 diff --git a/.config/git/base.gitconfig b/.config/git/base.gitconfig new file mode 100644 index 0000000..3d71b4e --- /dev/null +++ b/.config/git/base.gitconfig @@ -0,0 +1,60 @@ +[gpg] + format = ssh + +[core] + editor = nvim + autocrlf = input + pager = delta + +[init] + defaultBranch = main + +[color] + ui = auto + +[pull] + rebase = true + +[push] + autoSetupRemote = true + +[merge] + tool = nvimdiff + +[mergetool] + prompt = false + keepBackup = false + +[interactive] + diffFilter = delta --color-only + +[delta] + navigate = true + line-numbers = true + side-by-side = true + +[commit] + gpgsign = true + +[tag] + gpgsign = true + +[alias] + s = status + st = status -sb + lg = log --oneline --graph --decorate --all + + aa = add -A + cm = commit -m + am = commit -am + + ps = push + pl = pull + + lo = log --oneline --graph --decorate + + unstage = reset HEAD -- + last = log -1 HEAD + oops = commit --amend --no-edit + + dot = "!git --git-dir=$HOME/.dotfiles --work-tree=$HOME" diff --git a/.config/kitty/kitty.conf b/.config/kitty/kitty.conf new file mode 100644 index 0000000..7e152f3 --- /dev/null +++ b/.config/kitty/kitty.conf @@ -0,0 +1,41 @@ +# WINDOW +window_padding_width 8 +hide_window_decorations yes +background_opacity 1.0 + +# SCROLLBACK +scrollback_lines 10000 +mouse_hide_wait -1 + +# FONT +font_size 9 +font_family JetBrainsMono Nerd Font +bold_font JetBrainsMono Nerd Font Bold +italic_font JetBrainsMono Nerd Font Italic +bold_italic_font JetBrainsMono Nerd Font Bold Italic + +# -COLOURS +foreground #cecdc3 +background #000000 +cursor_text_color #100f0f + +color0 #100f0f +color1 #af3029 +color2 #66800b +color3 #ad8301 +color4 #205ea6 +color5 #5e409d +color6 #24837b +color7 #cecdc3 + +color8 #282726 +color9 #d14d41 +color10 #879a39 +color11 #d0a215 +color12 #4385be +color13 #8b7ec8 +color14 #3aa99f +color15 #e6e4d9 + +# SHELL +#shell /home/dil/.local/bin/tmux-wrapper diff --git a/.config/lazygit/config.yml b/.config/lazygit/config.yml new file mode 100644 index 0000000..e69de29 diff --git a/.config/mako/config b/.config/mako/config new file mode 100644 index 0000000..8116529 --- /dev/null +++ b/.config/mako/config @@ -0,0 +1,41 @@ +# --- THE VOID: MAKO EDITION --- + +# Font & Text +font=JetBrainsMono Nerd Font 12 +text-color=#ffffff +width=350 +height=150 +margin=10 +padding=15 + +# The Box (Hard edges, glass background) +background-color=#000000cc +border-size=1 +border-color=#333333 +border-radius=0 + +# Icons +icons=1 +max-icon-size=48 +icon-location=left + +# Behavior +default-timeout=5000 +ignore-timeout=1 +layer=overlay +anchor=top-right + +# --- URGENCY OVERRIDES --- + +# Critical alerts (Inverted: White bg, Black text, just like your Critical Battery) +[urgency=critical] +background-color=#ffffff +text-color=#000000 +border-color=#ffffff + +# Low priority (Dimmer) +[urgency=low] +default-timeout=1500 +background-color=#1a1a1a +text-color=#888888 +border-color=#333333 diff --git a/.config/mpd/database b/.config/mpd/database new file mode 100644 index 0000000..3a5a9cd Binary files /dev/null and b/.config/mpd/database differ diff --git a/.config/mpd/mpd.conf b/.config/mpd/mpd.conf new file mode 100644 index 0000000..b5317f9 --- /dev/null +++ b/.config/mpd/mpd.conf @@ -0,0 +1,30 @@ +# FILES +music_directory "~/music" +playlist_directory "~/.config/mpd/playlists" +db_file "~/.config/mpd/database" +log_file "syslog" +pid_file "/tmp/mpd.pid" +state_file "~/.config/mpd/state" + +# NETWORK +bind_to_address "127.0.0.1" +port "6600" + +# AUDIO OUTPUT (PipeWire/Pulse) +audio_output { + type "pulse" + name "PulseAudio/PipeWire" +} + +# VISUALISER FEED +audio_output { + type "fifo" + name "visualiser" + path "/tmp/mpd.fifo" + format "44100:16:2" + enabled "yes" +} + +# TWEAKS +auto_update "yes" +restore_paused "yes" diff --git a/.config/mpv/input.conf b/.config/mpv/input.conf new file mode 100644 index 0000000..772c0ee --- /dev/null +++ b/.config/mpv/input.conf @@ -0,0 +1,43 @@ +# --- VIM NAVIGATION (Time) --- +h seek -5 # Left: Back 5s +l seek 5 # Right: Forward 5s +Shift+h seek -60 # H: Back 1 min +Shift+l seek 60 # L: Forward 1 min + +# --- VIM AUDIO (Volume) --- +k add volume 5 # Up: Volume Up +j add volume -5 # Down: Volume Down +m cycle mute # Mute + +# --- TRACK SELECTION (The Missing Part) --- +a cycle audio # Next Audio Track +Shift+a cycle audio down # Previous Audio Track +c cycle sub # Next Subtitle (Captions) +Shift+c cycle sub down # Previous Subtitle +v cycle sub-visibility # Hide/Show Subtitles + +# --- SYNC CORRECTION (Because rips aren't perfect) --- +z add sub-delay -0.1 # Shift subs earlier +Shift+z add sub-delay +0.1 # Shift subs later +- add audio-delay -0.1 # Shift audio earlier += add audio-delay +0.1 # Shift audio later + +# --- PLAYBACK CONTROL --- +SPACE cycle pause # Play/Pause +. frame-step # Next Frame +, frame-back-step # Previous Frame +> add speed 0.1 # Speed Up +< add speed -0.1 # Slow Down +BS set speed 1.0 # Reset Speed (Backspace) + +# --- POSITION & VIEW --- +0 seek 0 absolute # Go to Start +$ seek 100 absolute-percent # Go to End +f cycle fullscreen # Toggle Fullscreen +s screenshot # Take Screenshot +S screenshot video # Screenshot (No Subs) + +# --- UTILS --- +q quit-watch-later # Quit (Save position) +Shift+q quit # Quit (Discard position) +i script-binding stats/display-stats-toggle # Show Info diff --git a/.config/mpv/mpv.conf b/.config/mpv/mpv.conf new file mode 100644 index 0000000..6ffcbc3 --- /dev/null +++ b/.config/mpv/mpv.conf @@ -0,0 +1,25 @@ +# VIDEO AND PERFORMANCE +# use gpu for video output +vo=gpu +# enable safe hardware decoding to reduce cpu usage +hwdec=auto-safe +# keep the player window open after the file finishes +keep-open=yes + +# AUDIO BEHAVIOUR +# default startup volume +volume=100 +# maximum volume boost +volume-max=150 +# maintain original audio pitch when changing playback speed +audio-pitch-correction=yes + +# SUBTITLES AND LANGUAGES +# prefer english audio +alang=en,eng +# prefer english subtitles +slang=en,eng +# set subtitle text colour to white +sub-color="#FFFFFFFF" +# scale subtitles relative to window size +sub-scale-with-window=yes diff --git a/.config/ncmpcpp/config b/.config/ncmpcpp/config new file mode 100644 index 0000000..8591f4c --- /dev/null +++ b/.config/ncmpcpp/config @@ -0,0 +1,30 @@ +# --- CONNECTION --- +mpd_host = "127.0.0.1" +mpd_port = "6600" + +# --- VISUALIZER (The Cool Part) --- +visualizer_data_source = "/tmp/mpd.fifo" +visualizer_output_name = "my_fifo" +visualizer_in_stereo = "yes" +visualizer_type = "spectrum" # Options: spectrum, wave, ellipse +visualizer_look = "●●" # The characters for the graph + +# --- UI TWEAKS --- +playlist_display_mode = "columns" # Classic table view +browser_display_mode = "columns" +search_engine_display_mode = "columns" +playlist_editor_display_mode = "columns" +autocenter_mode = "yes" +centered_cursor = "yes" +user_interface = "alternative" # Cleaner look + +# --- COLORS (Arch-Droid Style) --- +colors_enabled = "yes" +main_window_color = "default" +header_window_color = "cyan" +volume_color = "green" +progressbar_color = "black" +progressbar_elapsed_color = "green" + +# --- SEEKING --- +seek_time = "5" diff --git a/.config/niri/autostart.kdl b/.config/niri/autostart.kdl new file mode 100644 index 0000000..507cfb3 --- /dev/null +++ b/.config/niri/autostart.kdl @@ -0,0 +1,22 @@ +// STARTUP DAEMONS +//spawn-at-startup "/usr/lib/polkit-kde-authentication-agent-1" +spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1" +spawn-at-startup "swaybg" "-i" "/home/sgbv/Pictures/turtle.jpg" "-m" "fill" +spawn-at-startup "waybar" +spawn-at-startup "mako" +spawn-at-startup "nm-applet" +spawn-at-startup "blueman-applet" +spawn-at-startup "wireplumber" +spawn-at-startup "pipewire" +spawn-at-startup "pipewire-pulse" +spawn-at-startup "sh" "-c" "mkdir -p /run/user/1000/keyring && gnome-keyring-daemon --start --components=secrets" + +// CLIPBOARD TRACKING +spawn-at-startup "wl-paste" "--type" "text" "--watch" "cliphist" "store" +spawn-at-startup "wl-paste" "--type" "image" "--watch" "cliphist" "store" + +// IDLE MANAGEMENT +spawn-at-startup "bash" "-c" "swayidle -w timeout 150 'brightnessctl -s set 10' resume 'brightnessctl -r' timeout 200 'swaylock -f -c 000000' timeout 250 'niri msg action power-off-monitors' resume 'niri msg action power-on-monitors' timeout 1800 'systemctl suspend' before-sleep 'swaylock -f -c 000000'" + +// BRIGHTNESS +spawn-at-startup "~/.local/bin/restore-brightness" diff --git a/.config/niri/binds.kdl b/.config/niri/binds.kdl new file mode 100644 index 0000000..b7274c7 --- /dev/null +++ b/.config/niri/binds.kdl @@ -0,0 +1,95 @@ +// KEYBINDINGS +binds { + Mod+Return cooldown-ms=500 { spawn "alacritty"; } + Mod+Shift+Return cooldown-ms=500 { spawn "code"; } + Alt+Return cooldown-ms=500 {spawn "virt-manager"; } + Mod+Space { spawn "fuzzel"; } + Mod+Shift+Space { spawn "bash" "-c" "$HOME/.local/bin/fuzzel-run"; } + + // APPLICATION LAUNCHERS + Mod+B cooldown-ms=500 { spawn "librewolf"; } + Mod+Shift+B cooldown-ms=500 { spawn "firefox"; } + Mod+Alt+B cooldown-ms=500 { spawn "tor-browser"; } + Mod+O cooldown-ms=500 { spawn "obsidian"; } + Mod+A cooldown-ms=500 { spawn "enteauth"; } + Mod+Shift+A cooldown-ms=500 { spawn "keepassxc"; } + Mod+E cooldown-ms=500 { spawn "alacritty" "-e" "yazi"; } + Mod+Shift+E cooldown-ms=500 { spawn "nautilus"; } + Mod+G cooldown-ms=500 { spawn "azahar"; } + Mod+T cooldown-ms=500 { spawn "telegram-desktop"; } + + // TUI SYSTEM WORKPLACES + Mod+D cooldown-ms=500 { spawn "alacritty" "-e" "btop"; } + Mod+Shift+Y cooldown-ms=500 { spawn "alacritty" "-e" "lazygit"; } + Mod+M cooldown-ms=500 { spawn "alacritty" "-e" "ncmpcpp"; } + Mod+Shift+T cooldown-ms=500 { spawn "alacritty" "-e" "tut"; } + + // HARDWARE AND OVERLAYS + Mod+Shift+C { spawn "/home/sgbv/.local/bin/pick"; } + Mod+W { spawn-sh "killall -SIGUSR1 waybar"; } + + // WINDOW MECHANICS + Mod+Q repeat=false { close-window; } + Mod+Shift+Q { quit skip-confirmation=true; } + + // SPATIAL NAVIGATION + Mod+H { focus-column-left; } + Mod+L { focus-column-right; } + Mod+J { focus-window-down; } + Mod+K { focus-window-up; } + + Mod+Shift+H { move-column-left; } + Mod+Shift+L { move-column-right; } + Mod+Shift+J { move-window-down; } + Mod+Shift+K { move-window-up; } + + Mod+U { focus-workspace-down; } + Mod+I { focus-workspace-up; } + Mod+Shift+U { move-column-to-workspace-down; } + Mod+Shift+I { move-column-to-workspace-up; } + + // INTERFACE GEOMETRY + Mod+Minus { set-column-width "-10%"; } + Mod+Equal { set-column-width "+10%"; } + Mod+Shift+Minus { set-window-height "-10%"; } + Mod+Shift+Equal { set-window-height "+10%"; } + + Mod+Control+L { consume-window-into-column; } + Mod+Control+H { expel-window-from-column; } + + Mod+F { maximize-column; } + Mod+Shift+F { fullscreen-window; } + Mod+V { toggle-window-floating; } + + // NEWLY BOUND ACTIONS + Mod+Slash { show-hotkey-overlay; } + Mod+Tab { switch-preset-column-width; } + Mod+Shift+V { switch-focus-between-floating-and-tiling; } + Mod+R { open-overview; } + + // HARDWARE MEDIA ROUTING + Mod+Alt+K { spawn "~/.local/bin/volume-notify" "+10%"; } + Mod+Alt+J { spawn "~/.local/bin/volume-notify" "-10%"; } + XF86AudioRaiseVolume { spawn "~/.local/bin/volume-notify" "+5%"; } + XF86AudioLowerVolume { spawn "~/.local/bin/volume-notify" "-5%"; } + XF86AudioMute { spawn "~/.local/bin/volume-notify" "toggle"; } + XF86AudioMicMute { spawn "pamixer" "--default-source" "-t"; } + XF86MonBrightnessUp { spawn "~/.local/bin/brightness-notify" "+5%"; } + XF86MonBrightnessDown { spawn "~/.local/bin/brightness-notify" "5%-"; } + Mod+Alt+I { spawn "~/.local/bin/brightness-notify" "+10%"; } + Mod+Alt+U { spawn "~/.local/bin/brightness-notify" "10%-"; } + + + // MPC BINDS + Mod+Alt+Space { spawn "mpc" "toggle"; } + Mod+Alt+L { spawn "mpc" "next"; } + Mod+Alt+H { spawn "mpc" "prev"; } + Mod+Alt+Shift+K { spawn "mpc" "stop"; } + + Mod+Print { spawn "bash" "-c" "grim -g \"$(slurp)\" - | wl-copy"; } + Print { spawn "bash" "-c" "grim - | wl-copy"; } + Alt+Print { screenshot-window; } + Mod+C { spawn "bash" "-c" "cliphist list | fuzzel -d | cliphist decode | wl-copy"; } + Mod+Period { spawn "bash" "-c" "rofimoji --selector fuzzel --action type --clipboarder wl-copy"; } + Mod+Escape { spawn "~/.local/bin/power"; } +} diff --git a/.config/niri/config.kdl b/.config/niri/config.kdl new file mode 100644 index 0000000..a824ba1 --- /dev/null +++ b/.config/niri/config.kdl @@ -0,0 +1,52 @@ +// HARDWARE OUTPUT +output "eDP-1" { + scale 1.0 +} + +// INPUT CONFIGURATION +input { + keyboard { + xkb { + //layout "de" + layout "gb" + } + } + touchpad { + tap + natural-scroll + } + + focus-follows-mouse max-scroll-amount="0%" +} + +// APPEARANCE AND SPATIAL LAYOUT +layout { + gaps 2 + center-focused-column "never" + always-center-single-column + default-column-width { proportion 0.5; } + + border { + width 0.5 + active-color "rgb(61, 76, 7)" + inactive-color "rgb(26, 30, 12)" + } + + shadow { + off + } + + focus-ring { + off + } +} + +// MODULE INCLUSIONS +// merge external logical blocks +include "autostart.kdl" +include "rules.kdl" +include "binds.kdl" + +// MISCELLANEOUS CONFIGURATION +// disable saving screenshots to disk +screenshot-path null diff --git a/.config/niri/rules.kdl b/.config/niri/rules.kdl new file mode 100644 index 0000000..dc9db0c --- /dev/null +++ b/.config/niri/rules.kdl @@ -0,0 +1,154 @@ +// Floating windows +window-rule { + match app-id="blueman-manager" + match app-id="nm-connection-editor" + + open-floating true +} + +window-rule { + match app-id="firefox" title="Picture-in-Picture" + match app-id="librewolf" title="Picture-in-Picture" + + open-floating true + default-floating-position x=1466 y=44 + + min-width 439 + min-height 248 + max-width 439 + max-height 248 +} + +window-rule { + match app-id="io.ente.auth" + + open-floating true + default-floating-position x=1533 y=332 + + min-width 352 + min-height 447 + max-width 352 + max-height 447 +} + +window-rule { + match app-id="org.keepassxc.KeePassXC" + + open-floating true + default-floating-position x=1092 y=43 + + min-width 808 + min-height 535 + max-width 808 + max-height 535 +} + +window-rule { + match app-id="blueman-manager" + open-on-workspace "main" +} + +window-rule { + match app-id="nm-connection-editor" + open-on-workspace "main" +} + +// Maximized windows +window-rule { + match app-id="org.azahar_emu.Azahar" + open-maximized-to-edges true +} + +window-rule { + match app-id="Tor Browser" + open-maximized-to-edges true +} + +window-rule { + match app-id="Tor Browser" + open-maximized-to-edges true +} + +// Column widths +window-rule { + match app-id="Alacritty" + default-column-width { proportion 0.60; } +} + +window-rule { + match app-id="org.gnome.Nautilus" + default-column-width { proportion 0.60; } +} + +window-rule { + match app-id="code" + default-column-width { proportion 0.60; } +} + +window-rule { + match app-id="firefox" + default-column-width { proportion 0.40; } +} + +window-rule { + match app-id="librewolf" + default-column-width { proportion 0.40; } +} + +// Privacy +window-rule { + match app-id="Tor Browser" + + block-out-from "screencast" +} + +// Workspace placement +window-rule { + match app-id="Alacritty" + open-on-workspace "main" +} + +window-rule { + match app-id="org.gnome.Nautilus" + open-on-workspace "main" +} + +window-rule { + match app-id="code" + open-on-workspace "main" +} + +window-rule { + match app-id="firefox" + open-on-workspace "main" +} + +window-rule { + match app-id="librewolf" + open-on-workspace "main" +} + +window-rule { + match app-id="md.Obsidian" + open-on-workspace "gaming" +} + +window-rule { + match app-id="org.azahar_emu.Azahar" + open-on-workspace "gaming" +} + +window-rule { + match app-id="org.telegram.desktop" + open-on-workspace "private" +} + +window-rule { + match app-id="Tor Browser" + open-on-workspace "private" +} + +// Workspaces +workspace "main" +workspace "gaming" +workspace "private" diff --git a/.config/nvim/init.lua b/.config/nvim/init.lua new file mode 100644 index 0000000..0b0006d --- /dev/null +++ b/.config/nvim/init.lua @@ -0,0 +1,157 @@ +-- CORE SETTINGS +-- define leader key +vim.g.mapleader = " " +vim.g.maplocalleader = " " + +-- editor behaviour +vim.opt.number = true +vim.opt.relativenumber = true +vim.opt.mouse = "a" +vim.opt.ignorecase = true +vim.opt.smartcase = true +vim.opt.updatetime = 250 +vim.opt.termguicolors = true + +-- wayland clipboard integration +vim.opt.clipboard = "unnamedplus" + +-- regional standardisation +vim.opt.spelllang = "en_gb" + +-- PACKAGE MANAGER +-- bootstrap lazy nvim +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not vim.loop.fs_stat(lazypath) then + vim.fn.system({ + "git", + "clone", + "--filter=blob:none", + "https://github.com/folke/lazy.nvim.git", + "--branch=stable", + lazypath, + }) +end +vim.opt.rtp:prepend(lazypath) + +-- PLUGINS +-- load flexoki and core utilities +require("lazy").setup({ + { + "kepano/flexoki-neovim", + name = "flexoki", + priority = 1000, + config = function() + vim.cmd("colorscheme flexoki-dark") + -- override specific highlights to match your sway environment + vim.api.nvim_set_hl(0, "Normal", { bg = "#100F0F" }) + vim.api.nvim_set_hl(0, "NormalFloat", { bg = "#100F0F" }) + end, + }, + { + "nvim-treesitter/nvim-treesitter", + build = ":TSUpdate", + config = function() + -- INSTALLATION + -- trigger parser downloads + require("nvim-treesitter").install({ "lua", "bash", "markdown", "markdown_inline" }) + + -- HIGHLIGHTING + -- activate native treesitter engine per filetype + vim.api.nvim_create_autocmd("FileType", { + pattern = { "lua", "sh", "markdown" }, + callback = function() + vim.treesitter.start() + end, + }) + end, + }, + { + "hrsh7th/nvim-cmp", + dependencies = { + "hrsh7th/cmp-path", + "hrsh7th/cmp-buffer", + }, + config = function() + local cmp = require("cmp") + + -- COMPLETION ENGINE SETUP + -- initialise strictly local sources for paths and buffer text + cmp.setup({ + mapping = cmp.mapping.preset.insert({ + [""] = cmp.mapping.scroll_docs(-4), + [""] = cmp.mapping.scroll_docs(4), + [""] = cmp.mapping.complete(), + [""] = cmp.mapping.abort(), + [""] = cmp.mapping.confirm({ select = true }), + }), + sources = cmp.config.sources({ + { name = "path" }, + { name = "buffer" }, + }) + }) + end, + }, + { + "akinsho/bufferline.nvim", + dependencies = { "nvim-tree/nvim-web-devicons" }, + config = function() + -- BUFFER VISUALISATION + -- render background buffers as a top navigational bar + require("bufferline").setup({ + options = { + mode = "buffers", + numbers = "ordinal", + show_buffer_close_icons = false, + show_close_icon = false, + separator_style = "thin", + enforce_regular_tabs = true, + always_show_bufferline = true, + } + }) + + -- KEY MAPS + -- bind shift-h and shift-l to cycle through the open buffers + vim.keymap.set("n", "", "BufferLineCyclePrev", { desc = "previous buffer" }) + vim.keymap.set("n", "", "BufferLineCycleNext", { desc = "next buffer" }) + + -- buffer termination + -- bind leader-x to safely unload the current buffer from memory + vim.keymap.set("n", "x", "bdelete", { desc = "close current buffer" }) + end, + }, + { + "MeanderingProgrammer/render-markdown.nvim", + dependencies = { + "nvim-treesitter/nvim-treesitter", + "nvim-tree/nvim-web-devicons" + }, + opts = { + heading = { + sign = false, + icons = { '󰲡 ', '󰲣 ', '󰲥 ', '󰲧 ', '󰲩 ', '󰲫 ' }, + }, + code = { + sign = false, + width = "block", + right_pad = 1, + }, + }, + }, + { + "folke/zen-mode.nvim", + config = function() + require("zen-mode").setup({ + window = { + width = 80, + options = { + number = false, + relativenumber = false, + } + }, + }) + + -- map leader-z to toggle the writing mode + vim.keymap.set("n", "z", "ZenMode", { desc = "toggle zen mode" }) + end + } +}) diff --git a/.config/nvim/lazy-lock.json b/.config/nvim/lazy-lock.json new file mode 100644 index 0000000..8cd5ce9 --- /dev/null +++ b/.config/nvim/lazy-lock.json @@ -0,0 +1,12 @@ +{ + "bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" }, + "cmp-buffer": { "branch": "main", "commit": "b74fab3656eea9de20a9b8116afa3cfc4ec09657" }, + "cmp-path": { "branch": "main", "commit": "c642487086dbd9a93160e1679a1327be111cbc25" }, + "flexoki": { "branch": "main", "commit": "c3e2251e813d29d885a7cbbe9808a7af234d845d" }, + "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, + "nvim-cmp": { "branch": "main", "commit": "a1d504892f2bc56c2e79b65c6faded2fd21f3eca" }, + "nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" }, + "nvim-web-devicons": { "branch": "master", "commit": "7d8dad2c837fcdf2e65997e68657c1a21c568074" }, + "render-markdown.nvim": { "branch": "main", "commit": "629eb9533ec989d9d5c6cab8f3ad5372422c24e0" }, + "zen-mode.nvim": { "branch": "main", "commit": "8564ce6d29ec7554eb9df578efa882d33b3c23a7" } +} diff --git a/.config/nwg-displays/config b/.config/nwg-displays/config new file mode 100644 index 0000000..e0d8396 --- /dev/null +++ b/.config/nwg-displays/config @@ -0,0 +1,9 @@ +{ + "view-scale": 0.15, + "snap-threshold": 10, + "indicator-timeout": 500, + "custom-mode": [], + "use-desc": false, + "confirm-timeout": 10, + "profile-bound-wallpapers": true +} \ No newline at end of file diff --git a/.config/nwg-look/config b/.config/nwg-look/config new file mode 100644 index 0000000..50cd4cf --- /dev/null +++ b/.config/nwg-look/config @@ -0,0 +1,10 @@ +{ + "export-settings-ini": true, + "export-gtkrc-20": true, + "export-index-theme": true, + "export-xsettingsd": true, + "export-gtk4-symlinks": true, + "flatpak-export-gtk-theme-override": false, + "flatpak-export-icon-theme-override": false, + "flatpak-install-current-gtk-theme": false +} \ No newline at end of file diff --git a/.config/qt5ct/qt5ct.conf b/.config/qt5ct/qt5ct.conf new file mode 100644 index 0000000..149acad --- /dev/null +++ b/.config/qt5ct/qt5ct.conf @@ -0,0 +1,2 @@ +[SettingsWindow] +geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\xff\xff\xff\xfd\xff\xff\xff\xe2\0\0\x3\xb2\0\0\x4\r\0\0\0\0\0\0\0\0\0\0\x3\xaf\0\0\x4\n\0\0\0\0\0\0\0\0\a\x80\0\0\0\0\0\0\0\0\0\0\x3\xaf\0\0\x4\n) diff --git a/.config/qt6ct/qt6ct.conf b/.config/qt6ct/qt6ct.conf new file mode 100644 index 0000000..149acad --- /dev/null +++ b/.config/qt6ct/qt6ct.conf @@ -0,0 +1,2 @@ +[SettingsWindow] +geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\xff\xff\xff\xfd\xff\xff\xff\xe2\0\0\x3\xb2\0\0\x4\r\0\0\0\0\0\0\0\0\0\0\x3\xaf\0\0\x4\n\0\0\0\0\0\0\0\0\a\x80\0\0\0\0\0\0\0\0\0\0\x3\xaf\0\0\x4\n) diff --git a/.config/starship.toml b/.config/starship.toml new file mode 100644 index 0000000..dbeaf12 --- /dev/null +++ b/.config/starship.toml @@ -0,0 +1,55 @@ +# PROMPT LAYOUT +# define the left and right structural order +format = "$python$directory$git_branch$git_status$hostname$character" +right_format = """$jobs$time""" + +# PYTHON ENVIRONMENT +# display the active virtual environment name without the python version +[python] +style = "green" +format = "[$virtualenv]($style) " + +# DIRECTORY PATH +# display the truncated current working path +[directory] +style = "cyan" +truncate_to_repo = true +truncation_length = 3 +format = "[$path]($style) " + +# GIT BRANCH +# display the current repository branch +[git_branch] +format = "[$branch]($style) " +style = "yellow" + +# GIT STATUS +# display modification indicators +[git_status] +style = "red" +format = "[$all_status]($style) " + +# HOSTNAME +# display the machine name only during ssh sessions +[hostname] +ssh_only = true +format = "[$hostname](blue) " + +# PROMPT CHARACTER +# display stateful execution symbols +[character] +success_symbol = "[>](bold green)" +error_symbol = "[x](bold red)" +vicmd_symbol = "[v](bold green)" + +# BACKGROUND JOBS +# indicate processes suspended in the background +[jobs] +symbol = "✦ " + +# SYSTEM TIME +# display the current time on the right axis +[time] +disabled = false +time_format = "%T" +format = "[$time]($style)" diff --git a/.config/swaylock/config b/.config/swaylock/config new file mode 100644 index 0000000..e6ba99f --- /dev/null +++ b/.config/swaylock/config @@ -0,0 +1,43 @@ +# CORE AESTHETICS +# base background colour (flexoki ink dark) +color=100f0f +font=JetBrainsMono Nerd Font +font-size=24 + +# INDICATOR GEOMETRY +indicator-radius=120 +indicator-thickness=12 +indicator-idle-visible + +# INTERNAL RING COLOURS +inside-color=100f0f +inside-clear-color=100f0f +inside-ver-color=100f0f +inside-wrong-color=100f0f + +# EXTERNAL RING COLOURS +# normal state (grey) +ring-color=cecdc3 +# clearing state (yellow) +ring-clear-color=d0a215 +# verifying state (purple) +ring-ver-color=8b7ec8 +# incorrect state (red) +ring-wrong-color=af3029 + +# KEYPRESS HIGHLIGHTS +# highlight colour when typing (green) +key-hl-color=879a39 +# highlight colour when backspacing (yellow) +bshl-color=d0a215 + +# TEXT COLOURS +text-color=cecdc3 +text-clear-color=100f0f +text-ver-color=cecdc3 +text-wrong-color=cecdc3 + +# BEHAVIOUR +# disable the caps lock warning to maintain minimalism +ignore-empty-password +show-failed-attempts diff --git a/.config/tmux/tmux.conf b/.config/tmux/tmux.conf new file mode 100644 index 0000000..96ba963 --- /dev/null +++ b/.config/tmux/tmux.conf @@ -0,0 +1,28 @@ +# CORE ENVIRONMENT +set -g prefix C-b +bind C-b send-prefix + +set -g base-index 1 +set -g pane-base-index 1 +set -g renumber-windows on + +set -g default-terminal "tmux-256color" + +set -g mouse on +set -g mode-keys vi + +# STATUS BAR +set -g status-position top +set -g status-justify right + +set -g status-style bg="#000000",fg="#cecdc3" + +set -g window-status-format " #I:#W " +set -g window-status-current-format " #I:#W " +set -g window-status-current-style bg="#282726",fg="#8b7ec8",bold + +set -g status-left " [#S] " +set -g status-left-length 40 + +set -g status-right "" +set -g status-right-length 80 diff --git a/.config/waybar/config b/.config/waybar/config new file mode 100644 index 0000000..e562605 --- /dev/null +++ b/.config/waybar/config @@ -0,0 +1,68 @@ +{ + "layer": "top", + "position": "top", + "height": 28, + "spacing": 8, + + "modules-left": ["niri/workspaces", "niri/window", "mpd"], + "modules-center": ["clock"], + "modules-right": ["custom/bandwidth", "battery", "tray"], + + "niri/window": { + "format": "{}", + "rewrite": { + "(.*) — Mozilla Firefox": "Firefox", + "(.*) — LibreWolf": "Librewolf", + "(.*) - Obsidian v(.*)": "Obsidian" + }, + "max-length": 50 + }, + + "niri/workspaces": { + "format": "{icon}", + "format-icons": { + "focused": "●", + "default": "○" + } + }, + + "clock": { + "format": "{:%H:%M | %d}", + "tooltip-format": "{:%Y %B}\n{calendar}" + }, + + "mpd": { + "format": "{stateIcon} {artist} - {title}", + "format-disconnected": " offline", + "format-stopped": " stopped", + "state-icons": { + "paused": "", + "playing": "" + }, + "max-length": 40, + "tooltip-format": "MPD (connected)", + "tooltip-format-disconnected": "MPD (disconnected)", + "on-click": "mpc toggle", + "on-scroll-up": "mpc next", + "on-scroll-down": "mpc prev", + "on-click-right": "alacritty -e ncmpcpp" + }, + + "custom/bandwidth": { + "exec": "~/.local/bin/waybar-net", + "return-type": "json", + "interval": 60, + "format": "{}" + }, + + "battery": { + "states": { "warning": 30, "critical": 15 }, + "format": "{icon} {capacity}%", + "format-icons": ["󰂎", "󰁺", "󰁼", "󰁾", "󰂀", "󰂂", "󰁹"] + }, + + "tray": { + "icon-size": 14, + "spacing": 10 + } +} diff --git a/.config/waybar/style.css b/.config/waybar/style.css new file mode 100644 index 0000000..f49ba77 --- /dev/null +++ b/.config/waybar/style.css @@ -0,0 +1,104 @@ +* { + font-family: "JetBrainsMono Nerd Font", "Roboto", sans-serif; + font-size: 13px; + min-height: 0; + border: none; + border-radius: 0; +} + +/* BASE WINDOW */ +/* edge-to-edge dark translucent bar */ +window#waybar { + background-color: rgba(15, 15, 15, 0.85); + color: #cdd6f4; +} + +/* WORKSPACES */ +#workspaces { + margin-left: 8px; +} + +#workspaces button { + padding: 0 8px; + color: #555555; + background: transparent; + transition: all 0.2s ease; +} + +/* flexoki yellow for active workspaces */ +#workspaces button.focused, +#workspaces button.active { + color: #d0a215; +} + +#workspaces button:hover { + box-shadow: inherit; + text-shadow: inherit; + color: #aaaaaa; +} + +/* MODULE SPACING */ +#clock, +#custom, +#network, +#bluetooth, +#pulseaudio, +#backlight, +#battery, +#tray, +#window { + padding: 0 12px; + opacity: 0.9; +} + +/* HARDWARE COLOURS */ +/* assigning distinct muted hues to logical blocks */ +#clock { + color: #babbf1; +} + +#network { + color: #8caaee; +} + +#bluetooth { + color: #ca9ee6; +} + +#pulseaudio { + color: #81c8be; +} + +#backlight { + color: #e5c890; +} + +#battery { + color: #a6d189; +} + +#battery.charging { + color: #a6e3a1; +} + +#battery.critical:not(.charging) { + color: #e78284; +} + +/* MPD CONTROLS */ +#mpd { + padding: 0 10px; + font-family: monospace; + color: #cdd6f4; +} + +/* dim the text when inactive */ +#mpd.disconnected, +#mpd.stopped { + color: #6c7086; +} + +/* green hue when active */ +#mpd.playing { + color: #a6e3a1; +} diff --git a/.config/zathura/zathurarc b/.config/zathura/zathurarc new file mode 100644 index 0000000..a977999 --- /dev/null +++ b/.config/zathura/zathurarc @@ -0,0 +1,20 @@ +# ZATHURA CONFIGURATION +# disable automatic recolouring on startup +set recolor "false" + +# COLOUR MAPPINGS +# preserve image hues when recolouring is activated +set recolor-keephue "true" + +# recolour variables applied only when toggled +set recolor-lightcolor "#1e1e2e" +set recolor-darkcolor "#cdd6f4" + +# USER INTERFACE +# static dark theme for the application chrome +set default-bg "#1e1e2e" +set default-fg "#cdd6f4" +set statusbar-bg "#1e1e2e" +set statusbar-fg "#cdd6f4" + +unmap d diff --git a/.local/bin/README.md b/.local/bin/README.md new file mode 100755 index 0000000..1430678 --- /dev/null +++ b/.local/bin/README.md @@ -0,0 +1,29 @@ +# UTILS + +a collection of personal utility scripts for arch linux system management, automation, and infrastructure reconnaissance. + +## DEPENDENCIES + +most scripts rely on the following packages available in the arch repositories or aur: + +`bash`, `python3`, `jq`, `curl`, `bc`, `awk`, `bind`, `whois`, `openssl`, `aria2`, `yt-dlp`, `ffmpeg`, `fzf`, `lm_sensors`, `pacman-contrib`, `rsync`, `python-watchdog`, `waybar` + +## UTILITIES ARCHITECTURE + +the repository is structured around singular, verb-oriented tools designed for autonomous infrastructure management: + +* `archive`: handles universal file compression and extraction routing. +* `fetch`: executes autonomous media liberation via aria2c and yt-dlp. +* `rate`: provides terminal-native fiat and crypto currency evaluation. +* `recon`: executes domain reconnaissance and infrastructure security auditing. +* `status`: audits arch linux system telemetry and environment health. +* `pick`: provides modular element selection functionality. +* `power`: handles system state and operational power management. +* `scratch`: controls the window manager scratchpad buffer. +* `audit-pkg`: scans local package environments for vulnerabilities. +* `audit-users`: maps system permissions and active user accounts. +* `backup-obsd`: automates synchronisation for local knowledge vaults. +* `obsd`: launches the local knowledge base environment. +* `adbx`: wraps android debug bridge operations. +* `fuzzel-run`: configures the autonomous application launcher. +* `waybar-net`: feeds network state data to the status bar. diff --git a/.local/bin/adbx b/.local/bin/adbx new file mode 100755 index 0000000..6866676 --- /dev/null +++ b/.local/bin/adbx @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +# COLOURS +red='\033[0;31m' +green='\033[0;32m' +yellow='\033[1;33m' +blue='\033[0;34m' +cyan='\033[0;36m' +nc='\033[0m' + +# LOGGING +log_info() { printf "${blue}[info]${nc} %s\n" "$1"; } +log_success() { printf "${green}[ok]${nc} %s\n" "$1"; } +log_warn() { printf "${yellow}[warn]${nc} %s\n" "$1"; } +log_err() { printf "${red}[err]${nc} %s\n" "$1"; } + +# ENVIRONMENT +check_deps() { + local tools=(adb fastboot apktool uber-apk-signer fzf) + for t in "${tools[@]}"; do + if ! command -v "$t" &>/dev/null; then + log_warn "$t missing" + fi + done +} + +# DEVICE MANAGEMENT +cmd_info() { + printf "${cyan}DEVICE INFORMATION${nc}\n" + printf "model: %s\n" "$(adb shell getprop ro.product.model)" + printf "android: %s\n" "$(adb shell getprop ro.build.version.release)" + + local bat=$(adb shell dumpsys battery | grep level | cut -d: -f2 | tr -d ' ') + local stat=$(adb shell dumpsys battery | grep 'AC powered' | cut -d: -f2 | tr -d ' ') + [[ "$stat" == "true" ]] && charge="charging" || charge="discharging" + + printf "battery: %s%% (%s)\n" "$bat" "$charge" + + if adb shell su -c "echo 'rooted'" &>/dev/null; then + printf "root: ${green}yes${nc}\n" + else + printf "root: ${red}no${nc}\n" + fi +} + +cmd_wifi() { + local port=${1:-5555} + local ip=$(adb shell ip addr show wlan0 | grep 'inet ' | awk '{print $2}' | cut -d/ -f1) + if [ -z "$ip" ]; then + log_err "wifi ip not detected" + return 1 + fi + adb tcpip $port + sleep 2 + adb connect "$ip:$port" + log_success "connected to $ip:$port" +} + +# PACKAGE MANAGEMENT +cmd_nuke() { + local pkg=$(adb shell pm list packages | cut -d: -f2 | fzf --prompt="nuke > " --height=40%) + if [ -n "$pkg" ]; then + read -p "confirm nuke $pkg? (y/n): " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + adb shell pm uninstall -k --user 0 "$pkg" + log_success "nuked $pkg" + fi + fi +} + +# APK ENGINEERING +cmd_pull() { + local pkg=${1:-$(adb shell pm list packages | cut -d: -f2 | fzf --prompt="pull > ")} + [ -z "$pkg" ] && return + local path=$(adb shell pm path "$pkg" | head -n 1 | cut -d: -f2 | tr -d '\r') + log_info "pulling $pkg..." + adb pull "$path" "./$pkg.apk" +} + +cmd_scrot() { + local file="scrot_$(date +%Y%m%d_%H%M%S).png" + adb shell screencap -p /sdcard/temp.png + adb pull /sdcard/temp.png "./$file" >/dev/null + adb shell rm /sdcard/temp.png + log_success "saved: $file" +} + +# MAIN ROUTER +usage() { + printf "${cyan}ARCH-DROID TOOL (ADBX)${nc}\n" + printf "usage: adbx [args]\n\n" + printf "DEVICE\n" + printf " check verify tools\n" + printf " list list devices\n" + printf " info show battery/root\n" + printf " wifi wireless adb\n\n" + printf "PACKAGES\n" + printf " nuke interactive uninstall\n" + printf " freeze disable app\n" + printf " melt enable app\n\n" + printf "FILES\n" + printf " pull extract apk\n" + printf " sign sign apk\n" + printf " scrot screenshot\n" + exit 1 +} + +case "$1" in +check) check_deps ;; +list) + adb devices -l + fastboot devices + ;; +info) cmd_info ;; +wifi) cmd_wifi "$2" ;; +nuke) cmd_nuke ;; +pull) cmd_pull "$2" ;; +scrot) cmd_scrot ;; +*) usage ;; +esac diff --git a/.local/bin/archive b/.local/bin/archive new file mode 100755 index 0000000..284dd65 --- /dev/null +++ b/.local/bin/archive @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -e + +# COLOURS +red='\033[1;31m' +green='\033[1;32m' +blue='\033[1;34m' +yellow='\033[1;33m' +nc='\033[0m' + +# USAGE GUIDE +# displays the help menu if arguments are missing or incorrect +show_help() { + printf "${blue}usage:${nc} archivectl \n\n" + printf "commands:\n" + printf " compress [output] compresses a file or directory\n" + printf " extract extracts a supported archive\n\n" + printf "examples:\n" + printf " archivectl compress myfolder\n" + printf " archivectl compress myfolder data.zip\n" + printf " archivectl extract data.tar.gz\n" + exit 1 +} + +# COMPRESSION ROUTINE +# handles the creation of new archives +do_compress() { + if [ -z "$1" ]; then + printf "${red}error: target missing for compression.${nc}\n" + exit 1 + fi + + local source_path="${1%/}" + if [ ! -e "$source_path" ]; then + printf "${red}error: source '%s' not found.${nc}\n" "$source_path" + exit 1 + fi + + local output + if [ -n "$2" ]; then + output="$2" + else + output="${source_path}.tar.gz" + fi + + printf "${blue}compressing ${yellow}%s${nc} into ${yellow}%s${nc}...\n" "$source_path" "$output" + + case "$output" in + *.tar.gz | *.tgz) tar -czf "$output" "$source_path" ;; + *.tar.xz) tar -cJf "$output" "$source_path" ;; + *.tar.bz2) tar -cjf "$output" "$source_path" ;; + *.tar) tar -cf "$output" "$source_path" ;; + *.zip) zip -r "$output" "$source_path" ;; + *.7z) 7z a "$output" "$source_path" ;; + *.rar) rar a "$output" "$source_path" ;; + *) + printf "${red}error: format not recognised.${nc}\n" + exit 1 + ;; + esac + + printf "${green}compression complete.${nc}\n" + du -h "$output" | awk '{print $1 " " $2}' +} + +# EXTRACTION ROUTINE +# handles the unpacking of existing archives +do_extract() { + if [ -z "$1" ]; then + printf "${red}error: target missing for extraction.${nc}\n" + exit 1 + fi + + local file="$1" + if [ ! -f "$file" ]; then + printf "${red}error: file '%s' not found.${nc}\n" "$file" + exit 1 + fi + + printf "${blue}extracting: ${yellow}%s${nc}...\n" "$file" + + case "$file" in + *.tar.bz2 | *.tbz2) tar xjf "$file" ;; + *.tar.gz | *.tgz) tar xzf "$file" ;; + *.tar.xz) tar xJf "$file" ;; + *.tar) tar xf "$file" ;; + *.lzma) unlzma "$file" ;; + *.bz2) bunzip2 "$file" ;; + *.rar) unrar x "$file" ;; + *.gz) gunzip "$file" ;; + *.zip) unzip "$file" ;; + *.Z) uncompress "$file" ;; + *.7z) 7z x "$file" ;; + *.xz) unxz "$file" ;; + *.exe) cabextract "$file" ;; + *) + printf "${red}error: format not recognised.${nc}\n" + exit 1 + ;; + esac + + printf "${green}extraction complete.${nc}\n" +} + +# COMMAND ROUTER +# parses the first argument to determine execution path +case "$1" in + compress) + do_compress "$2" "$3" + ;; + extract) + do_extract "$2" + ;; + *) + show_help + ;; +esac diff --git a/.local/bin/audit-pkg b/.local/bin/audit-pkg new file mode 100755 index 0000000..1a0c96d --- /dev/null +++ b/.local/bin/audit-pkg @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +# GUARD +if ! command -v pacman &>/dev/null; then + echo "pacman not found. this script is for arch linux." >&2 + exit 1 +fi + +# COLOURS +red='\033[0;31m' +green='\033[0;32m' +yellow='\033[1;33m' +blue='\033[0;34m' +cyan='\033[0;36m' +bold='\033[1m' +dim='\033[2m' +nc='\033[0m' + +# COUNTERS +_covered=0 +_partial=0 +_missing=0 + +# HELPERS +is_installed() { + pacman -Qq "$1" &>/dev/null +} + +print_section() { + printf "\n${blue}${bold}%s${nc}\n" "$1" + printf "${dim}--------------------------------------------------${nc}\n" +} + +check_any() { + local category="$1" + local description="$2" + shift 2 + local -a found=() + for pkg in "$@"; do + is_installed "$pkg" && found+=("$pkg") + done + + if [[ ${#found[@]} -gt 0 ]]; then + printf " ${green}[ ok ]${nc} ${bold}%-35s${nc} ${dim}%s${nc}\n" "$category" "${found[0]}" + _covered=$((_covered + 1)) + else + printf " ${red}[ !! ]${nc} ${bold}%-35s${nc}\n" "$category" + printf " ${dim}%s${nc}\n" "$description" + _missing=$((_missing + 1)) + fi +} + +check_all() { + local category="$1" + local description="$2" + shift 2 + local -a found=() + local -a absent=() + for pkg in "$@"; do + if is_installed "$pkg"; then found+=("$pkg"); else absent+=("$pkg"); fi + done + + if [[ ${#absent[@]} -eq 0 ]]; then + printf " ${green}[ ok ]${nc} ${bold}%-35s${nc}\n" "$category" + _covered=$((_covered + 1)) + elif [[ ${#found[@]} -gt 0 ]]; then + printf " ${yellow}[ / ]${nc} ${bold}%-35s${nc}\n" "$category" + printf " ${dim}missing: %s${nc}\n" "${absent[*]}" + _partial=$((_partial + 1)) + else + printf " ${red}[ !! ]${nc} ${bold}%-35s${nc}\n" "$category" + printf " ${dim}need all of: %s${nc}\n" "$*" + _missing=$((_missing + 1)) + fi +} + +# HEADER +clear +printf "${cyan}${bold}ARCH LINUX + HYPRLAND AUDIT${nc}\n" +printf "${dim}checking package categories in local database${nc}\n\n" + +# CORE SYSTEM +print_section "CORE SYSTEM" +check_all "base system" "essential os foundation" base base-devel +check_any "kernel" "linux kernel flavour" linux linux-lts linux-zen linux-hardened +check_any "ucode" "processor microcode patches" intel-ucode amd-ucode +check_any "bootloader" "system startup manager" grub refind +check_any "filesystems" "fat/ntfs/ext4 utilities" dosfstools ntfs-3g e2fsprogs + +# DISPLAY AND COMPOSITOR +print_section "DISPLAY AND COMPOSITOR" +check_any "compositor" "hyprland or alternatives" hyprland sway niri +check_any "session manager" "uwsm or greetd" uwsm ly greetd +check_all "xdg portals" "screen sharing and file pickers" xdg-desktop-portal-hyprland xdg-desktop-portal-gtk +check_any "gpu drivers" "mesa or nvidia proprietary" mesa nvidia nvidia-open + +# DESKTOP COMPONENTS +print_section "DESKTOP COMPONENTS" +check_any "status bar" "waybar or eww" waybar eww ags +check_any "launcher" "app and command fuzzy search" rofi-wayland wofi fuzzel +check_any "notifications" "notification daemon" mako swaync dunst +check_any "locker" "screen locking utility" hyprlock swaylock +check_any "wallpaper" "wayland native wallpaper setter" hyprpaper swww +check_all "clipboard" "manager and history stack" wl-clipboard cliphist + +# AUDIO AND NETWORK +print_section "AUDIO AND NETWORK" +check_all "pipewire" "full audio stack" pipewire pipewire-pulse pipewire-alsa wireplumber +check_any "network" "networkmanager or iwd" networkmanager iwd +check_any "firewall" "traffic control" ufw nftables + +# SHELL AND EDITORS +print_section "SHELL AND EDITORS" +check_any "terminal" "wayland native terminal" kitty alacritty foot ghostty +check_any "shell" "bash or zsh" bash zsh fish +check_any "prompt" "shell prompt customiser" starship oh-my-posh +check_any "editor" "modal or simple text editor" nvim vim micro nano + +# INPUT AND POWER +print_section "INPUT AND POWER" +check_any "brightness" "backlight control" brightnessctl light +check_any "power" "power management daemon" tlp auto-cpufreq +check_all "bluetooth" "bluetooth stack" bluez bluez-utils +check_any "bt-manager" "bluetooth frontend" blueman bluetui + +# THEMING AND FONTS +print_section "THEMING AND FONTS" +check_any "nerd-font" "glyphs for terminal icons" ttf-jetbrains-mono-nerd ttf-hack-nerd +check_any "emoji" "emoji rendering support" noto-fonts-emoji +check_any "gtk-theme" "gtk appearance manager" nwg-look lxappearance + +# SECURITY AND SECRETS +print_section "SECURITY AND SECRETS" +check_any "polkit" "auth agent for elevated tasks" polkit-kde-agent lxqt-policykit +check_any "keyring" "password and secret storage" gnome-keyring + +# SUMMARY +_total=$((_covered + _partial + _missing)) +printf "\n${blue}${bold}SUMMARY${nc}\n" +printf "${dim}--------------------------------------------------${nc}\n" +printf " ${green}ok: %d${nc}\n" "$_covered" +printf " ${yellow}partial: %d${nc}\n" "$_partial" +printf " ${red}missing: %d${nc}\n" "$_missing" +printf " total: %d\n\n" "$_total" diff --git a/.local/bin/audit-users b/.local/bin/audit-users new file mode 100755 index 0000000..c000ad3 --- /dev/null +++ b/.local/bin/audit-users @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# COLOURS +cyan='\033[0;36m' +nc='\033[0m' + +# USER AUDIT +printf "${cyan}AUTHORISED HUMAN USERS${nc}\n" +printf "%-12s %-6s %-15s %-30s %s\n" "user" "uid" "shell" "groups" "last login" + +# filter for uids between 1000 and 60000 +awk -F: '$3 >= 1000 && $3 < 60000 {print $1}' /etc/passwd | while read -r user; do + uid=$(id -u "$user") + groups=$(groups "$user" | cut -d: -f2 | xargs) + shell=$(getent passwd "$user" | cut -d: -f7) + + # parse lastlog for the second line and strip the username to get the timestamp + last_raw=$(lastlog -u "$user" | awk 'NR==2 { $1=""; print $0 }' | xargs) + + printf "%-12s %-6s %-15s %-30s %s\n" \ + "$user" "$uid" "$shell" "$groups" "${last_raw:-never logged in}" +done diff --git a/.local/bin/backup-obsd b/.local/bin/backup-obsd new file mode 100755 index 0000000..0a8622c --- /dev/null +++ b/.local/bin/backup-obsd @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# AUTHOR: Daesorin +# CREATED: 2026-07-28 16:26 +0100 + +set -euo pipefail + +echo "creating tarball" +echo "" +tar --zstd -cf - -C ~ obsidian | age -r age1y5fnkaugdmqgegurgq6jdqrm5lz3ehyp9ve7rhf69pmr9xy9veyqtnappe -o ~/vault-$(date "+%FT%T").tar.zst.age +echo "" +echo "pushing tarball to filen" +echo "" +filen upload ~/vault-*.tar.zst.age /obsidian +echo "" +echo "deleting tarbal" +rm -rf ~/vault-*.tar.zst.age +echo "" +echo "obsidian vault backup complete" +echo "" +filen ls -l /obsidian diff --git a/.local/bin/brightness-notify b/.local/bin/brightness-notify new file mode 100755 index 0000000..c7134a0 --- /dev/null +++ b/.local/bin/brightness-notify @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# AUTHOR: Daesorin +# CREATED: 2026-08-04 05:02 +0100 + +set -euo pipefail + +# ADJUST BACKLIGHT +brightnessctl set "$@" > /dev/null + +# FETCH CURRENT PERCENTAGE +brightness=$(brightnessctl -m | awk -F, '{print $4}' | tr -d '%') + +notify-send -h string:x-canonical-private-synchronous:brightness -u low "Brightness: ${brightness}%" diff --git a/.local/bin/bw b/.local/bin/bw new file mode 100755 index 0000000..ae44578 --- /dev/null +++ b/.local/bin/bw @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +SESSION=$(secret-tool lookup service bitwarden 2>/dev/null) + +if [[ -z "$SESSION" ]]; then + SESSION=$(bw unlock --raw) + secret-tool store --label="bw-session" service bitwarden <<< "$SESSION" +fi + +exec /usr/bin/bw "$@" --session "$SESSION" diff --git a/.local/bin/fetch b/.local/bin/fetch new file mode 100755 index 0000000..cad1beb --- /dev/null +++ b/.local/bin/fetch @@ -0,0 +1,250 @@ +#!/usr/bin/env bash + +# COLOURS +red='\033[1;31m' +green='\033[1;32m' +blue='\033[1;34m' +cyan='\033[1;36m' +yellow='\033[1;33m' +nc='\033[0m' + +# CONFIGURATION +session_file="$HOME/.aria2_session" +xdg_videos="${XDG_VIDEOS_DIR:-$HOME/Videos}" +xdg_music="${XDG_MUSIC_DIR:-$HOME/Music}" +hist_video="$HOME/.config/archive_history_video.txt" +hist_music="$HOME/.config/archive_history_music.txt" + +# establish configuration directory +mkdir -p "$HOME/.config" +touch "$session_file" + +# UTILITIES +log() { + local col="$1" + local msg="$2" + printf "${col}%s${nc}\n" "$msg" +} + +# verify core binaries and clipboard providers +check_dependencies() { + local deps=("aria2c" "yt-dlp" "spotdl") + for dep in "${deps[@]}"; do + if ! command -v "$dep" >/dev/null 2>&1; then + log "$red" "missing core dependency: $dep" + exit 1 + fi + done + + if ! command -v wl-paste >/dev/null 2>&1 && ! command -v xclip >/dev/null 2>&1; then + log "$yellow" "warning: missing wl-clipboard or xclip. clipboard polling disabled." + fi +} + +# extract link from clipboard +get_clipboard_url() { + local content="" + + if command -v wl-paste >/dev/null 2>&1; then + content=$(wl-paste) + elif command -v xclip >/dev/null 2>&1; then + content=$(xclip -o -selection clipboard) + fi + + content="$(echo "$content" | xargs)" + + [[ "$content" == http* ]] && echo "$content" +} + +# ARIA2C CORE +run_aria2c() { + aria2c \ + --continue=true \ + --max-connection-per-server=16 \ + --split=16 \ + --min-split-size=1M \ + --user-agent="Mozilla/5.0" \ + --save-session="$session_file" \ + --save-session-interval=60 \ + --seed-time=0 \ + "$@" || log "$red" "aria2c failed" +} + +# SPOTIFY CORE +run_spotdl() { + local url="$1" + local target_dir="$2" + + log "$cyan" "spotify routing to $target_dir" + + mkdir -p "$target_dir" + cd "$target_dir" || return 1 + + local cmd=( + spotdl download "$url" + --format opus + --output "{artists} - {title}.{ext}" + ) + + local attempt + for attempt in 1 2 3; do + log "$cyan" "spotdl execution attempt $attempt" + + if timeout 300 "${cmd[@]}"; then + return 0 + fi + + log "$yellow" "spotdl network or api failure on attempt $attempt" + sleep 2 + done + + log "$red" "spotdl failed permanently due to api restrictions" + return 1 +} + +# YT-DLP CORE +run_ytdlp() { + local url="$1" + local target_dir="$2" + local hist_file="$3" + local out_tmpl="$4" + shift 4 + local extra_args=("$@") + + log "$cyan" "media routing to $target_dir" + + mkdir -p "$target_dir" + + yt-dlp \ + --progress \ + --ignore-errors \ + --add-metadata \ + --embed-thumbnail \ + --embed-subs \ + --embed-chapters \ + --downloader aria2c \ + --downloader-args "aria2c:-x 4 -k 1M" \ + --download-archive "$hist_file" \ + -o "${target_dir}/${out_tmpl}" \ + "${extra_args[@]}" \ + "$url" \ + || log "$yellow" "yt-dlp finished with errors" +} + +# MODES +mode_media() { + local url="$1" + local media_type="$2" + local format_arg="$3" + local playlist_flag="$4" + + local audio_tmpl="%(playlist_index|)s%(playlist_index& - |)s%(artist,uploader|Unknown)s - %(title)s [%(id)s].%(ext)s" + local video_tmpl="%(playlist_index|)s%(playlist_index& - |)s%(uploader,channel|Unknown)s - %(title)s [%(id)s].%(ext)s" + + local extra_args=() + [[ -n "$playlist_flag" ]] && extra_args+=("$playlist_flag") + + if [[ "$media_type" == "audio" ]]; then + run_ytdlp "$url" "$xdg_music" "$hist_music" "$audio_tmpl" \ + "${extra_args[@]}" \ + --extract-audio --audio-format opus --audio-quality 0 + else + if [[ "$format_arg" == "best" ]]; then + run_ytdlp "$url" "$xdg_videos" "$hist_video" "$video_tmpl" \ + "${extra_args[@]}" -f "bv*+ba/b" --merge-output-format mkv + + elif [[ "$format_arg" == "ask" ]]; then + log "$cyan" "querying upstream for format manifests" + yt-dlp -F "$url" + printf "\n${cyan}enter desired format code:${nc} " + read -r selected_format + + if [[ -n "$selected_format" ]]; then + run_ytdlp "$url" "$xdg_videos" "$hist_video" "$video_tmpl" \ + "${extra_args[@]}" -f "$selected_format" --merge-output-format mkv + else + log "$red" "aborted: no format code provided" + exit 1 + fi + else + run_ytdlp "$url" "$xdg_videos" "$hist_video" "$video_tmpl" \ + "${extra_args[@]}" -f "$format_arg" --merge-output-format mkv + fi + fi +} + +# BATCH MODE +mode_batch() { + local filepath="$1" + + [[ ! -f "$filepath" ]] && { + log "$red" "batch file missing: $filepath" + exit 1 + } + + log "$blue" "batch mode execution for $filepath" + + local tmp + tmp=$(mktemp) + + # strip comments and empty lines + grep -vE '^\s*#|^\s*$' "$filepath" > "$tmp" + + run_aria2c --input-file="$tmp" --max-concurrent-downloads=4 + + rm -f "$tmp" +} + +# ARG PARSING +target="" +media_type="video" +format_arg="best" +playlist_flag="" +resume=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + -t|--type) + media_type="$2"; shift 2 ;; + -f|--format) + format_arg="$2"; shift 2 ;; + -p|--playlist) + playlist_flag="--yes-playlist"; shift ;; + -r|--resume) + resume=1; shift ;; + *) + [[ -z "$target" ]] && target="$1" + shift ;; + esac +done + +# ROUTER +check_dependencies + +[[ -z "$target" ]] && target=$(get_clipboard_url) + +if [[ -z "$target" && $resume -eq 0 ]]; then + log "$yellow" "usage: fetch [url|file] [-t audio|video] [-f best|ask|code] [-p]" + exit 1 +fi + +if [[ $resume -eq 1 ]]; then + log "$yellow" "resuming global aria2 session" + run_aria2c --input-file="$session_file" + +elif [[ -f "$target" ]]; then + mode_batch "$target" + +elif [[ "$target" =~ \.(iso|zip|rar|tar|gz|bz2|7z|torrent|pdf|png|jpg|mp4|mkv)$ ]]; then + log "$blue" "direct file payload detected" + run_aria2c "$target" + +elif [[ "$target" =~ spotify\.com ]]; then + run_spotdl "$target" "$xdg_music" || { + log "$red" "spotdl failed. yt-dlp cannot natively parse walled-garden spotify urls. manual extraction required." + exit 1 + } + +else + mode_media "$target" "$media_type" "$format_arg" "$playlist_flag" +fi diff --git a/.local/bin/fuzzel-run b/.local/bin/fuzzel-run new file mode 100755 index 0000000..be39aae --- /dev/null +++ b/.local/bin/fuzzel-run @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# Generate a list of all commands, remove duplicates, and pipe to Fuzzel +selected=$(compgen -c | sort -u | fuzzel -d --prompt="RUN: ") + +# Execute the selected binary through the compositor +[ -n "$selected" ] && swaymsg exec -- "$selected" diff --git a/.local/bin/hledger-charts b/.local/bin/hledger-charts new file mode 100755 index 0000000..79507ab --- /dev/null +++ b/.local/bin/hledger-charts @@ -0,0 +1,669 @@ +#!/usr/bin/env python3 +# AUTHOR: Daesorin +# CREATED: 2026-07-12 09:40 +0100 +# UPDATED: 2026-07-27 10:53 +0100 + +""" +hledger-charts + +terminal and image charts for hledger csv output. reads csv from standard +input, piped directly from hledger, and renders an ascii chart to the +terminal, a matplotlib png, or both. + +ARCH DIFFERENCE +matplotlib is only required for --png output, ascii charts use the standard +library alone. install matplotlib via pacman rather than pip where possible: + + pacman -S python-matplotlib + +pip installs into an externally managed environment fail under PEP 668 +unless a virtualenv is used or --break-system-packages is passed. + +INPUT +pipe hledger csv output in directly, or pass --journal so the script +calls hledger itself. pass the -O csv flag when piping directly from +hledger. the --format flag specifies a custom line format string and +produces incompatible text: + + hledger balance -O csv | hledger-charts pie --png pie.png + hledger-charts pie --journal .journal/2026.journal --png pie.png + +histogram and pie read a single period balance report. trend, stacked, and +grouped read a multi period balance report (hledger balance -M), which the +script requests automatically when --journal is given. pass an account pattern +or date range as trailing query arguments to focus the chart. all trailing +arguments and flags after the chart options are passed straight through to +hledger balance: + + hledger-charts stacked --journal .journal/2026.journal expenses + hledger-charts trend --journal .journal/2026.journal expenses -b 2026-01 -e 2026-04 +""" + +import argparse +import csv +import io +import os +import re +import subprocess +import sys + +SPARK_CHARS = "▁▂▃▄▅▆▇█" +ANSI_COLOURS = [31, 33, 32, 36, 34, 35, 91, 93, 92, 96, 94, 95] + + +# AMOUNT PARSING +# shared by both single period and multi period csv shapes + +def clean_amount(raw, invert=False): + """EXTRACT THE FIRST NUMERIC VALUE FROM CURRENCY AND FORMATTED STRINGS + accepts formats such as "1,200.00 NGN", "NGN -1,200.00", "- 450.00" + """ + cleaned = raw.replace(",", "") + cleaned = re.sub(r"([-+])\s+", r"\1", cleaned) + match = re.search(r"[-+]?\d+(?:\.\d+)?", cleaned) + if not match: + return 0.0 + val = float(match.group(0)) + return -val if invert else val + + +# ACCOUNT HIERARCHY + +def rollup_account(account, depth): + """TRUNCATE ACCOUNT NAME TO A SPECIFIED HIERARCHY DEPTH""" + if not depth or depth <= 0: + return account + parts = account.split(":") + return ":".join(parts[:depth]) + + +def aggregate_single_period(accounts, amounts, depth): + """GROUP SINGLE PERIOD AMOUNTS BY TRUNCATED ACCOUNT NAME""" + if not depth or depth <= 0: + return accounts, amounts + grouped = {} + for acc, amt in zip(accounts, amounts): + parent = rollup_account(acc, depth) + grouped[parent] = grouped.get(parent, 0.0) + amt + return list(grouped.keys()), list(grouped.values()) + + +def aggregate_multi_period(accounts, periods, matrix, depth): + """GROUP MULTI PERIOD MATRICES BY TRUNCATED ACCOUNT NAME""" + if not depth or depth <= 0: + return accounts, periods, matrix + grouped = {} + n_periods = len(periods) + for i, acc in enumerate(accounts): + parent = rollup_account(acc, depth) + if parent not in grouped: + grouped[parent] = [0.0] * n_periods + for j in range(n_periods): + grouped[parent][j] += matrix[i][j] + return list(grouped.keys()), periods, list(grouped.values()) + + +# CSV PARSING + +def parse_balance_csv(rows, invert=False): + """PARSE A SINGLE PERIOD BALANCE REPORT + expects rows shaped like [account, amount], skips headers and totals + """ + accounts = [] + amounts = [] + for row in rows: + if len(row) < 2: + continue + account = row[0].strip() + if account.lower().rstrip(" :") in ("account", "total", ""): + continue + try: + amount = clean_amount(row[1], invert=invert) + except ValueError: + continue + accounts.append(account) + amounts.append(amount) + return accounts, amounts + + +def parse_period_csv(rows, invert=False): + """PARSE A MULTI PERIOD BALANCE REPORT + expects a header of account,period1,... and drops any trailing total column + """ + if not rows: + return [], [], [] + header = [c.strip() for c in rows[0]] + if len(header) < 2: + raise ValueError( + "period csv needs an account column and at least one value column" + ) + periods = header[1:] + if len(periods) > 1 and periods[-1].lower().rstrip(" :") == "total": + periods = periods[:-1] + accounts = [] + matrix = [] + for row in rows[1:]: + if len(row) < 2: + continue + account = row[0].strip() + if account.lower().rstrip(" :") in ("account", "total", ""): + continue + values = row[1:1 + len(periods)] + try: + parsed = [clean_amount(v, invert=invert) for v in values] + except ValueError: + continue + accounts.append(account) + matrix.append(parsed) + return accounts, periods, matrix + + +# DATA LOADING + +def load_csv_rows(args, period): + """LOAD BALANCE REPORT ROWS WITH TTY AND PIPELINE VALIDATION + calls hledger directly when --journal is given, otherwise reads csv from stdin + """ + if not args.journal: + if sys.stdin.isatty(): + print("error: no input piped to stdin and --journal not specified", file=sys.stderr) + sys.exit(1) + if args.query: + print("warning: query arguments are ignored when reading pre-computed csv from stdin", file=sys.stderr) + return list(csv.reader(sys.stdin)) + cmd = ["hledger", "-f", args.journal, "balance", "-O", "csv"] + if period: + cmd.append("-M") + cmd.extend(args.query) + try: + result = subprocess.run(cmd, capture_output=True, text=True) + except FileNotFoundError: + print("error: hledger not found on PATH", file=sys.stderr) + sys.exit(1) + if result.returncode != 0: + print(f"error: hledger exited with status {result.returncode}", file=sys.stderr) + if result.stderr: + print(result.stderr.strip(), file=sys.stderr) + sys.exit(1) + return list(csv.reader(io.StringIO(result.stdout))) + + +# RANKING HELPERS +# keep charts readable when the account list is long + +def top_n_with_other(labels, values, n): + """COLLAPSE LOW RANKED ENTRIES INTO A SINGLE OTHER CATEGORY""" + paired = sorted(zip(labels, values), key=lambda p: abs(p[1]), reverse=True) + if n <= 0 or len(paired) <= n: + return [p[0] for p in paired], [p[1] for p in paired] + top = paired[:n] + rest = paired[n:] + other_total = sum(v for _, v in rest) + out_labels = [p[0] for p in top] + out_values = [p[1] for p in top] + if other_total != 0: + out_labels.append("other") + out_values.append(other_total) + return out_labels, out_values + + +def top_n_matrix_with_other(accounts, matrix, n): + """COLLAPSE LOW RANKED ACCOUNTS INTO AN OTHER ROW ACROSS ALL PERIODS""" + totals = [sum(abs(v) for v in row) for row in matrix] + order = sorted(range(len(accounts)), key=lambda i: totals[i], reverse=True) + if n <= 0 or len(order) <= n: + idx = order + return [accounts[i] for i in idx], [matrix[i] for i in idx] + top_idx = order[:n] + rest_idx = order[n:] + top_accounts = [accounts[i] for i in top_idx] + top_matrix = [matrix[i] for i in top_idx] + n_periods = len(matrix[0]) if matrix else 0 + other_row = [sum(matrix[i][j] for i in rest_idx) for j in range(n_periods)] + if any(other_row): + top_accounts.append("other") + top_matrix.append(other_row) + return top_accounts, top_matrix + + +# ASCII RENDERING PRIMITIVES + +def use_colour(args): + if args.no_colour: + return False + if os.environ.get("NO_COLOR"): + return False + return sys.stdout.isatty() + + +def sparkline(values): + """RENDER A COMPACT UNICODE SPARKLINE FOR ONE SERIES""" + if not values: + return "" + lo = min(values) + hi = max(values) + span = hi - lo + if span == 0: + return SPARK_CHARS[0] * len(values) + out = [] + for v in values: + idx = int(((v - lo) / span) * (len(SPARK_CHARS) - 1)) + out.append(SPARK_CHARS[idx]) + return "".join(out) + + +def multiline_sparkline(values, rows=2): + """RENDER A MULTI-LINE ASCII SPARKLINE CANVAS""" + if not values or rows <= 1: + return [sparkline(values)] + lo = min(values) + hi = max(values) + span = hi - lo if hi != lo else 1.0 + canvas = [[" " for _ in values] for _ in range(rows)] + for col, v in enumerate(values): + norm = (v - lo) / span + scaled = norm * rows + full_blocks = int(scaled) + remainder = scaled - full_blocks + for r in range(full_blocks): + if r < rows: + canvas[rows - 1 - r][col] = "█" + if full_blocks < rows: + idx = int(remainder * len(SPARK_CHARS)) + if idx > 0 or full_blocks == 0: + canvas[rows - 1 - full_blocks][col] = SPARK_CHARS[min(idx, len(SPARK_CHARS) - 1)] + return ["".join(row) for row in canvas] + + +def segment_glyph(i, colour): + """PICK A GLYPH FOR SEGMENT I + coloured terminals reuse a plain block since colour carries the + distinction, plain terminals fall back to a letter per segment so + adjacent segments of the same rendered width stay distinguishable + """ + if colour: + return "█" + return chr(ord("A") + (i % 26)) + + +def render_stacked_bar(labels, values, width, colour): + """RENDER ONE PROPORTIONAL STACKED BAR ACROSS THE GIVEN WIDTH""" + abs_values = [abs(v) for v in values] + total = sum(abs_values) + if total <= 0: + return " " * width + segments = [] + allocated = 0 + for i, v in enumerate(abs_values): + seg_width = int(round((v / total) * width)) + if i == len(abs_values) - 1: + seg_width = max(width - allocated, 0) + allocated += seg_width + glyph = segment_glyph(i, colour) + char = glyph * seg_width + if colour: + code = ANSI_COLOURS[i % len(ANSI_COLOURS)] + char = f"\033[{code}m{char}\033[0m" + segments.append(char) + return "".join(segments) + + +def render_legend(labels, values, colour): + """RENDER A LABEL AND PERCENTAGE LEGEND""" + abs_values = [abs(v) for v in values] + total = sum(abs_values) + max_label = max((len(l) for l in labels), default=0) + lines = [] + for i, (label, value) in enumerate(zip(labels, values)): + pct = (abs(value) / total * 100) if total else 0.0 + glyph = segment_glyph(i, colour) + swatch = glyph + if colour: + code = ANSI_COLOURS[i % len(ANSI_COLOURS)] + swatch = f"\033[{code}m{glyph}\033[0m" + lines.append(f"{swatch} {label:<{max_label}} {value:>14.2f} {pct:>5.1f}%") + return "\n".join(lines) + + +# MATPLOTLIB HELPERS +# imported lazily, ascii output never needs this dependency + +def _matplotlib_pyplot(theme="default"): + try: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + if theme and theme != "default": + try: + plt.style.use(theme) + except OSError: + print(f"warning: matplotlib theme '{theme}' not found, falling back to default", file=sys.stderr) + return plt + except ImportError: + print( + "error: matplotlib not installed, install with " + "'pacman -S python-matplotlib' or " + "'pip install matplotlib --break-system-packages'", + file=sys.stderr, + ) + sys.exit(1) + + +def render_histogram_png(accounts, amounts, path, currency, theme): + plt = _matplotlib_pyplot(theme) + data = sorted(zip(accounts, amounts), key=lambda p: abs(p[1]), reverse=True) + labels = [d[0] for d in data] + values = [d[1] for d in data] + fig, ax = plt.subplots(figsize=(10, max(4, len(labels) * 0.4))) + ax.barh(labels, values, color="#4C72B0") + ax.invert_yaxis() + ax.set_xlabel(f"amount ({currency})" if currency else "amount") + ax.set_title("account balances") + fig.tight_layout() + fig.savefig(path, dpi=150) + fig.clf() + plt.close(fig) + print(f"saved histogram to {path}", file=sys.stderr) + + +# MATPLOTLIB HELPERS + +def render_pie_png(labels, values, path, currency, theme): + plt = _matplotlib_pyplot(theme) + fig, ax = plt.subplots(figsize=(10, 7)) + abs_values = [abs(v) for v in values] + total = sum(abs_values) + + # format legend labels with percentages + legend_labels = [ + f"{label} ({(abs(val) / total * 100):.1f}%)" if total else label + for label, val in zip(labels, values) + ] + + # suppress autopct on small slices to prevent overlap + def autopct_fmt(pct): + return f"{pct:.1f}%" if pct >= 3.0 else "" + + wedges, _, autotexts = ax.pie( + abs_values, + autopct=autopct_fmt, + startangle=90, + pctdistance=0.75, + ) + + # adjust text contrast based on slice luminance + for wedge, autotext in zip(wedges, autotexts): + if autotext.get_text(): + r, g, b = wedge.get_facecolor()[:3] + luminance = 0.299 * r + 0.587 * g + 0.114 * b + autotext.set_color("black" if luminance > 0.6 else "white") + autotext.set_fontsize(9) + autotext.set_weight("bold") + + ax.legend( + wedges, + legend_labels, + title="accounts", + loc="center left", + bbox_to_anchor=(1, 0, 0.5, 1), + frameon=False, + ) + + ax.set_title( + f"account distribution ({currency})" + if currency + else "account distribution" + ) + ax.axis("equal") + fig.tight_layout() + fig.savefig(path, dpi=150, bbox_inches="tight") + fig.clf() + plt.close(fig) + print(f"saved pie chart to {path}", file=sys.stderr) + + +def render_trend_png(accounts, periods, matrix, path, currency, theme): + plt = _matplotlib_pyplot(theme) + fig, ax = plt.subplots(figsize=(10, 6)) + for i, account in enumerate(accounts): + ax.plot(periods, matrix[i], marker="o", label=account) + ax.set_xlabel("period") + ax.set_ylabel(f"amount ({currency})" if currency else "amount") + ax.set_title("balance trend") + ax.legend() + plt.setp(ax.get_xticklabels(), rotation=45, ha="right") + fig.tight_layout() + fig.savefig(path, dpi=150) + fig.clf() + plt.close(fig) + print(f"saved trend chart to {path}", file=sys.stderr) + + +def render_stacked_png(accounts, periods, matrix, path, currency, theme): + plt = _matplotlib_pyplot(theme) + fig, ax = plt.subplots(figsize=(10, 6)) + bottom = [0.0] * len(periods) + for i, account in enumerate(accounts): + values = [abs(v) for v in matrix[i]] + ax.bar(periods, values, bottom=bottom, label=account) + bottom = [b + v for b, v in zip(bottom, values)] + ax.set_xlabel("period") + ax.set_ylabel(f"amount ({currency})" if currency else "amount") + ax.set_title("spending by category per period") + ax.legend() + plt.setp(ax.get_xticklabels(), rotation=45, ha="right") + fig.tight_layout() + fig.savefig(path, dpi=150) + fig.clf() + plt.close(fig) + print(f"saved stacked chart to {path}", file=sys.stderr) + + +def render_grouped_png(accounts, periods, matrix, path, currency, theme): + plt = _matplotlib_pyplot(theme) + x = list(range(len(periods))) + n = len(accounts) + width = 0.8 / max(n, 1) + fig, ax = plt.subplots(figsize=(10, 6)) + for i, account in enumerate(accounts): + values = [abs(v) for v in matrix[i]] + offset = (i - (n - 1) / 2) * width + positions = [xi + offset for xi in x] + ax.bar(positions, values, width=width, label=account) + ax.set_xticks(x) + ax.set_xticklabels(periods, rotation=45, ha="right") + ax.set_xlabel("period") + ax.set_ylabel(f"amount ({currency})" if currency else "amount") + ax.set_title("monthly comparison by account") + ax.legend() + fig.tight_layout() + fig.savefig(path, dpi=150) + fig.clf() + plt.close(fig) + print(f"saved grouped chart to {path}", file=sys.stderr) + + +# SUBCOMMANDS + +def cmd_histogram(args): + rows = load_csv_rows(args, period=False) + accounts, amounts = parse_balance_csv(rows, invert=args.invert) + if not accounts: + print("error: no account data found in input", file=sys.stderr) + sys.exit(1) + accounts, amounts = aggregate_single_period(accounts, amounts, args.depth) + total = sum(amounts) + abs_total = sum(abs(a) for a in amounts) + data = sorted(zip(accounts, amounts), key=lambda p: abs(p[1]), reverse=True) + max_label = max(len(a) for a, _ in data) + bar_width = 35 + print(f"\n{'ACCOUNT':<{max_label}} | {'AMOUNT':>14} | {'%':>5} | HISTOGRAM") + print("-" * (max_label + 62)) + for account, amount in data: + pct = (abs(amount) / abs_total * 100) if abs_total else 0.0 + filled = int((pct / 100) * bar_width) + print(f"{account:<{max_label}} | {amount:>14.2f} | {pct:>4.1f}% | {'█' * filled}") + print("-" * (max_label + 62)) + print(f"{'TOTAL':<{max_label}} | {total:>14.2f} | 100.0% |\n") + if args.png: + render_histogram_png(accounts, amounts, args.png, args.currency, args.theme) + + +def cmd_pie(args): + rows = load_csv_rows(args, period=False) + accounts, amounts = parse_balance_csv(rows, invert=args.invert) + if not accounts: + print("error: no account data found in input", file=sys.stderr) + sys.exit(1) + accounts, amounts = aggregate_single_period(accounts, amounts, args.depth) + labels, values = top_n_with_other(accounts, amounts, args.top) + colour = use_colour(args) + print() + print(render_stacked_bar(labels, values, args.width, colour)) + print() + print(render_legend(labels, values, colour)) + print() + if args.png: + render_pie_png(labels, values, args.png, args.currency, args.theme) + + +def cmd_trend(args): + rows = load_csv_rows(args, period=True) + accounts, periods, matrix = parse_period_csv(rows, invert=args.invert) + if not accounts: + print("error: no account data found in input", file=sys.stderr) + sys.exit(1) + accounts, periods, matrix = aggregate_multi_period(accounts, periods, matrix, args.depth) + if args.top > 0: + accounts, matrix = top_n_matrix_with_other(accounts, matrix, args.top) + else: + # DEFAULT TO A SINGLE AGGREGATE SERIES + # sums every account per period unless --top requests individual lines + agg = [sum(row[j] for row in matrix) for j in range(len(periods))] + accounts, matrix = ["total"], [agg] + colour = use_colour(args) + max_label = max(len(a) for a in accounts) + print() + for i, account in enumerate(accounts): + series = matrix[i] + spark_lines = multiline_sparkline(series, rows=args.rows) + if colour: + code = ANSI_COLOURS[i % len(ANSI_COLOURS)] + spark_lines = [f"\033[{code}m{sl}\033[0m" for sl in spark_lines] + if args.rows > 1: + print(f"{account}: {series[0]:>12.2f} -> {series[-1]:>12.2f}") + for sl in spark_lines: + print(f" {sl}") + print() + else: + print(f"{account:<{max_label}} {spark_lines[0]} {series[0]:>12.2f} -> {series[-1]:>12.2f}") + if args.rows <= 1: + print() + print("periods: " + ", ".join(periods)) + print() + if args.png: + render_trend_png(accounts, periods, matrix, args.png, args.currency, args.theme) + + +def cmd_stacked(args): + rows = load_csv_rows(args, period=True) + accounts, periods, matrix = parse_period_csv(rows, invert=args.invert) + if not accounts: + print("error: no account data found in input", file=sys.stderr) + sys.exit(1) + accounts, periods, matrix = aggregate_multi_period(accounts, periods, matrix, args.depth) + accounts, matrix = top_n_matrix_with_other(accounts, matrix, args.top) + colour = use_colour(args) + max_period = max(len(p) for p in periods) + print() + for j, period in enumerate(periods): + values = [abs(matrix[i][j]) for i in range(len(accounts))] + bar = render_stacked_bar(accounts, values, args.width, colour) + print(f"{period:<{max_period}} {bar}") + print() + totals = [sum(abs(matrix[i][j]) for j in range(len(periods))) for i in range(len(accounts))] + print(render_legend(accounts, totals, colour)) + print() + if args.png: + render_stacked_png(accounts, periods, matrix, args.png, args.currency, args.theme) + + +def cmd_grouped(args): + rows = load_csv_rows(args, period=True) + accounts, periods, matrix = parse_period_csv(rows, invert=args.invert) + if not accounts: + print("error: no account data found in input", file=sys.stderr) + sys.exit(1) + accounts, periods, matrix = aggregate_multi_period(accounts, periods, matrix, args.depth) + accounts, matrix = top_n_matrix_with_other(accounts, matrix, args.top) + colour = use_colour(args) + bar_width = 20 + print() + for i, account in enumerate(accounts): + row = [abs(v) for v in matrix[i]] + row_max = max(row) if row else 0 + print(account) + for j, period in enumerate(periods): + filled = int((row[j] / row_max) * bar_width) if row_max else 0 + char = "█" * filled + if colour: + code = ANSI_COLOURS[i % len(ANSI_COLOURS)] + char = f"\033[{code}m{char}\033[0m" + padded = char + " " * (bar_width - filled) + print(f" {period:<10} {padded} {row[j]:>12.2f}") + print() + if args.png: + render_grouped_png(accounts, periods, matrix, args.png, args.currency, args.theme) + + +# ARGUMENT PARSING + +def build_parser(): + parser = argparse.ArgumentParser( + description="terminal and image charts for hledger csv output, reads csv from standard input" + ) + sub = parser.add_subparsers(dest="command", required=True) + + common = argparse.ArgumentParser(add_help=False) + common.add_argument("--png", metavar="PATH", help="also render a matplotlib png to this path") + common.add_argument("-t", "--theme", default="default", help="matplotlib stylesheet theme (e.g., dark_background, ggplot)") + common.add_argument("--currency", default="", help="currency label for chart titles") + common.add_argument("--no-colour", action="store_true", help="disable ansi colour output") + common.add_argument("-i", "--invert", action="store_true", help="invert mathematical signs (multiply all amounts by -1)") + common.add_argument("-d", "--depth", type=int, default=0, help="summarise accounts to this hierarchy depth (0 for full names)") + common.add_argument("--journal", metavar="PATH", help="hledger journal file, calls hledger directly instead of reading csv from stdin") + common.add_argument("query", nargs=argparse.REMAINDER, help="optional hledger query passed through to hledger balance, e.g. an account pattern or -b/-e dates, only used with --journal") + + p_hist = sub.add_parser("histogram", parents=[common], help="horizontal bar histogram of account balances") + p_hist.set_defaults(func=cmd_histogram) + + p_pie = sub.add_parser("pie", parents=[common], help="proportional bar and pie chart of account distribution") + p_pie.add_argument("--top", type=int, default=8, help="accounts to show before collapsing into other, 0 for no limit") + p_pie.add_argument("--width", type=int, default=60, help="width of the ascii bar in characters") + p_pie.set_defaults(func=cmd_pie) + + p_trend = sub.add_parser("trend", parents=[common], help="balance trend over time as sparklines and a line chart") + p_trend.add_argument("--top", type=int, default=0, help="individual account lines to show, 0 shows only the total") + p_trend.add_argument("-r", "--rows", type=int, default=1, help="height of the ascii sparkline canvas in rows (default 1)") + p_trend.set_defaults(func=cmd_trend) + + p_stack = sub.add_parser("stacked", parents=[common], help="stacked bar of spending by category per period") + p_stack.add_argument("--top", type=int, default=8, help="accounts to show before collapsing into other, 0 for no limit") + p_stack.add_argument("--width", type=int, default=50, help="width of each ascii bar in characters") + p_stack.set_defaults(func=cmd_stacked) + + p_group = sub.add_parser("grouped", parents=[common], help="grouped bar comparison of accounts across periods") + p_group.add_argument("--top", type=int, default=6, help="accounts to show before collapsing into other, 0 for no limit") + p_group.set_defaults(func=cmd_grouped) + + return parser + + +def main(): + parser = build_parser() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/.local/bin/md2csv b/.local/bin/md2csv new file mode 100755 index 0000000..6556632 --- /dev/null +++ b/.local/bin/md2csv @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# AUTHOR: Daesorin +# CREATED: 2026-07-28 11:14 +0100 + +import csv +import sys + +# INPUT VALIDATION +# ensure an input file argument is provided +if len(sys.argv) < 2: + print("usage: md2csv ") + sys.exit(1) + +input_file = sys.argv[1] + +# TABLE CONVERSION +# open input markdown and initialize standard output csv writer +with open(input_file, "r", encoding="utf-8") as infile: + writer = csv.writer(sys.stdout) + + for line in infile: + line = line.strip() + + # ROW FILTERING + # skip empty lines and markdown layout separator lines + if not line or "---" in line: + continue + + # CELL EXTRACTION + # check if line represents a markdown table row + if line.startswith("|") and line.endswith("|"): + # strip leading and trailing pipes then split by delimiter + raw_cells = line[1:-1].split("|") + cleaned_cells = [cell.strip() for cell in raw_cells] + writer.writerow(cleaned_cells) diff --git a/.local/bin/md2hledger b/.local/bin/md2hledger new file mode 100755 index 0000000..daab94b --- /dev/null +++ b/.local/bin/md2hledger @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# AUTHOR: Daesorin +# CREATED: 2026-07-27 + +import sys +from pathlib import Path +from decimal import Decimal, InvalidOperation + +# CONFIGURATION +# set default accounting buckets, fallback currency, and journal vault path +DEFAULT_EXPENSE_ACCOUNT = "Expenses:Uncategorised" +DEFAULT_INCOME_ACCOUNT = "Income:Uncategorised" +DEFAULT_PAYMENT_ACCOUNT = "Assets:Banks:OPay" +DEFAULT_CURRENCY = "NGN" +NOTES_DIR = Path("~/obsidian/finances").expanduser() + +# ACCOUNT NORMALISATION +# intercept legacy account strings and fix misrouted categories at compile time +def normalize_account(account_str, currency, name): + clean_account = account_str.strip() + clean_name = name.lower() + + # auto resolve generic raenest root postings to multi currency leaf accounts + if clean_account == "Assets:Banks:Raenest": + if currency == "USD": + return "Assets:Banks:Raenest:USD" + if currency == "GBP": + return "Assets:Banks:Raenest:GBP" + if currency == "EUR": + return "Assets:Banks:Raenest:EUR" + return "Assets:Banks:Raenest:NGN" + + # intercept fee and charge keywords before portfolio redirects + if any(w in clean_name for w in ("charge", "fee", "loss", "tax", "commission")): + if "uncategorised" in clean_account.lower(): + return "Expenses:Financial:BankLoss" + return clean_account + + # redirect uncategorised investment transfers to asset portfolios + if "uncategorised" in clean_account.lower(): + if "cowrywise" in clean_name: + return "Assets:Investments:Cowrywise" + if "bybit" in clean_name: + return "Assets:Investments:Bybit" + if "octafx" in clean_name: + return "Assets:Investments:OctaFX" + + return clean_account + +# FRONTMATTER PARSER +# extract yaml between the opening and closing dashes without external libraries +def parse_frontmatter(file_path): + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + except OSError as e: + print(f"warning: unable to read {file_path} ({e})", file=sys.stderr) + return None + + if not content.startswith("---"): + return None + + parts = content.split("---", 2) + if len(parts) < 3: + return None + + yaml_text = parts[1] + data = {} + for line in yaml_text.strip().split("\n"): + # ignore empty lines and full line comments + line = line.strip() + if not line or line.startswith("#"): + continue + + if ":" in line: + key, val = line.split(":", 1) + # strip inline yaml comments and surrounding quotation marks + clean_val = val.split("#", 1)[0].strip().strip('\'"') + data[key.strip().lower()] = clean_val + + return data + +# JOURNAL GENERATOR +# convert parsed dictionary into an hledger double entry transaction string +def generate_entry(data, file_path): + try: + date = data["date"] + name = data.get("name", "untitled transaction") + tx_type = data.get("type", "expense").lower() + + # read dynamic accounts or fallback to defaults + currency = data.get("currency", DEFAULT_CURRENCY).strip().upper() + raw_payment = data.get("payment_account", DEFAULT_PAYMENT_ACCOUNT) + raw_target = data.get("account", DEFAULT_EXPENSE_ACCOUNT if tx_type not in {"income", "liability", "borrowing", "inflow"} else DEFAULT_INCOME_ACCOUNT) + + # normalise accounts against currency and description keywords + payment_account = normalize_account(raw_payment, currency, name) + target_account = normalize_account(raw_target, currency, name) + + # use decimal for precise financial arithmetic + unit = Decimal(data.get("unit", "1")) + amount = Decimal(data["amount"]) + total = unit * amount + + # CHARGE RESOLUTION + # extract bank transfer fees and pos agent charges without multiplying by unit + transfer_charges = Decimal(data.get("transfer_charges", "0")) + pos_charges = Decimal(data.get("pos_charges", "0")) + + # FOREX CONVERSIONS + # handle dual currency exchanges and correct backwards conversion frontmatter + if tx_type == "conversion": + source_currency = data.get("source_currency", "USD").strip().upper() + source_amount = Decimal(data.get("source_amount", "0")) * unit + total_source_outflow = source_amount + + charge_postings = "" + if pos_charges > 0: + charge_postings += f" Expenses:Financial:POSFees {pos_charges:.2f} {source_currency}\n" + total_source_outflow += pos_charges + if transfer_charges > 0: + charge_postings += f" Expenses:Financial:BankLoss {transfer_charges:.2f} {source_currency}\n" + total_source_outflow += transfer_charges + + # fix reversed raenest conversion where foreign currency was logged as spent instead of local currency + if ("raenest" in name.lower() or "raenest" in payment_account.lower()) and source_currency != DEFAULT_CURRENCY: + entry = ( + f"{date} * {name}\n" + f" {payment_account} {source_amount:.2f} {source_currency}\n" + f" {target_account} -{total:.2f} {currency} @@ {source_amount:.2f} {source_currency}\n" + f"{charge_postings}\n" + ) + return entry + + entry = ( + f"{date} * {name}\n" + f" {target_account} {total:.2f} {currency}\n" + f"{charge_postings}" + f" {payment_account} -{total_source_outflow:.2f} {source_currency} @@ {total:.2f} {currency}\n\n" + ) + return entry + + # CASH FLOW ROUTING + # route transactions based on whether value enters or leaves the payment account + inflow_types = {"income", "liability", "borrowing"} + + # handle loans collected and standard inflows unless routed from equity windfalls + if tx_type == "inflow" and not payment_account.lower().startswith("equity"): + inflow_types.add("inflow") + + if tx_type in inflow_types: + entry = ( + f"{date} * {name}\n" + f" {payment_account} {currency} {total:.2f}\n" + f" {target_account} -{currency} {total:.2f}\n\n" + ) + else: + charge_postings = "" + total_outflow = total + + if pos_charges > 0: + charge_postings += f" Expenses:Financial:POSFees {currency} {pos_charges:.2f}\n" + total_outflow += pos_charges + + if transfer_charges > 0: + charge_postings += f" Expenses:Financial:BankLoss {currency} {transfer_charges:.2f}\n" + total_outflow += transfer_charges + + entry = ( + f"{date} * {name}\n" + f" {target_account} {currency} {total:.2f}\n" + f"{charge_postings}" + f" {payment_account} -{currency} {total_outflow:.2f}\n\n" + ) + + return entry + except (KeyError, InvalidOperation, TypeError) as e: + # log malformed notes to standard error to prevent silent data loss + print(f"warning: skipping malformed note '{file_path}' ({type(e).__name__})", file=sys.stderr) + return "" + +# MAIN EXECUTION +# scan the journal directory recursively and print compiled journal entries +if __name__ == "__main__": + if not NOTES_DIR.is_dir(): + print(f"error: directory '{NOTES_DIR}' does not exist", file=sys.stderr) + sys.exit(1) + + entries = [] + # rglob enables recursive scanning through nested folder structures + for file_path in sorted(NOTES_DIR.rglob("*.md")): + data = parse_frontmatter(file_path) + if data and "date" in data and "amount" in data: + entry = generate_entry(data, file_path) + if entry: + entries.append(entry) + + print("".join(entries), end="") diff --git a/.local/bin/obsd b/.local/bin/obsd new file mode 100755 index 0000000..eebbba2 --- /dev/null +++ b/.local/bin/obsd @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# AUTHOR: Daesorin +# CREATED: 2026-08-08 16:59 +0100 + +set -Eeuo pipefail + +# CONFIGURATION +LOCAL_VAULT="${HOME}/obsidian" +REMOTE_HOST="ish" +REMOTE_VAULT="~/obsidian" + +RSYNC_FLAGS=( + -az + --delete + --human-readable + --itemize-changes +) + +EXCLUDES=( + --exclude=".DS_Store" + --exclude=".Trash-*" + + # Obsidian cache + --exclude=".obsidian/cache/" + --exclude=".obsidian/workspace*.json" + --exclude=".obsidian/workspace-mobile.json" + + # Vim + --exclude="*.swp" + --exclude="*.swo" + + # Temporary files + --exclude="*~" +) + +# HELP +usage() { +cat < iSH + pull iSH -> Local + status Dry-run using timestamps + diff Dry-run using checksums + backup Create compressed archive +EOF +} + +# CHECKS +check_requirements() { + command -v rsync >/dev/null \ + || { echo "rsync not installed."; exit 1; } + + command -v ssh >/dev/null \ + || { echo "ssh not installed."; exit 1; } + + [[ -d "$LOCAL_VAULT" ]] \ + || { echo "Vault not found: $LOCAL_VAULT"; exit 1; } +} + +# BACKUP +backup() { + local backup_dir="${HOME}/obsidian-backups" + mkdir -p "$backup_dir" + + local stamp + stamp="$(date +%Y%m%d-%H%M%S)" + + local archive="${backup_dir}/obsidian-${stamp}.tar.zst" + + echo "Creating backup..." + + tar \ + --zstd \ + -cf "$archive" \ + -C "$HOME" \ + obsidian + + echo + echo "Backup created:" + echo " $archive" +} + +# PUSH +push() { + rsync \ + "${RSYNC_FLAGS[@]}" \ + "${EXCLUDES[@]}" \ + "${LOCAL_VAULT}/" \ + "${REMOTE_HOST}:${REMOTE_VAULT}/" +} + +# PULL +pull() { + rsync \ + "${RSYNC_FLAGS[@]}" \ + "${EXCLUDES[@]}" \ + "${REMOTE_HOST}:${REMOTE_VAULT}/" \ + "${LOCAL_VAULT}/" +} + +# STATUS +status() { + rsync \ + -azin \ + "${EXCLUDES[@]}" \ + "${LOCAL_VAULT}/" \ + "${REMOTE_HOST}:${REMOTE_VAULT}/" +} + +# DIFF +diff() { + rsync \ + -azcin \ + "${EXCLUDES[@]}" \ + "${LOCAL_VAULT}/" \ + "${REMOTE_HOST}:${REMOTE_VAULT}/" +} + +# MAIN +check_requirements + +case "${1:-help}" in + push) + push + ;; + pull) + pull + ;; + status) + status + ;; + diff) + diff + ;; + backup) + backup + ;; + help|-h|--help) + usage + ;; + *) + echo "Unknown command: ${1}" + echo + usage + exit 1 + ;; +esac diff --git a/.local/bin/obsd-bash-2026-06-03 b/.local/bin/obsd-bash-2026-06-03 new file mode 100755 index 0000000..80ed9ad --- /dev/null +++ b/.local/bin/obsd-bash-2026-06-03 @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +import argparse +import time +import subprocess +from threading import Timer +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler +from pathlib import Path + +# PRODUCTION CONFIGURATION +local_dir = "//home/sgbv/obsidian/" +remote_dest = "ish:/root/obsidian/" +debounce_seconds = 2.0 + +# ignore workspace ui states to prevent infinite sync loops +excludes = [ + "--exclude=.obsidian/workspace*", + "--exclude=.trash/", + "--exclude=.git/" +] + +def build_rsync_cmd(src, dest): + return ["rsync", "-avz", "--delete"] + excludes + [src, dest] + +def pull_sync(): + print(f"pulling changes from ish to arch...") + subprocess.run(build_rsync_cmd(remote_dest, local_dir)) + print("pull complete.") + +def push_sync(): + print(f"pushing changes from arch to ish...") + subprocess.run(build_rsync_cmd(local_dir, remote_dest)) + print("push complete.") + +class watchdog_push_handler(FileSystemEventHandler): + def __init__(self): + self.sync_timer = None + + def on_any_event(self, event): + # ignore directory changes and workspace/hidden cache updates + if event.is_directory or any(x in event.src_path for x in [".obsidian", ".trash"]): + return + + print(f"local change detected: {event.event_type}") + if self.sync_timer: + self.sync_timer.cancel() + + self.sync_timer = Timer(debounce_seconds, push_sync) + self.sync_timer.start() + +def watch_mode(): + print(f"arch watchdog listening on {local_dir}") + handler = watchdog_push_handler() + observer = Observer() + observer.schedule(handler, local_dir, recursive=True) + observer.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\nstopping watchdog...") + observer.stop() + observer.join() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="arch sync node") + parser.add_argument("mode", nargs="?", default="watch", choices=["watch", "push", "pull"]) + args = parser.parse_args() + + if args.mode == "pull": + pull_sync() + elif args.mode == "push": + push_sync() + else: + watch_mode() diff --git a/.local/bin/obsd-python-2026-07-26 b/.local/bin/obsd-python-2026-07-26 new file mode 100755 index 0000000..d66bb07 --- /dev/null +++ b/.local/bin/obsd-python-2026-07-26 @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# AUTHOR: Daesorin +# CREATED: 2026-06-03 20:12 +0100 +# UPDATED: 2026-07-24 09:40 +0100 +""" +The script was modified to replace the archive flag with explicit metadata-stripping and checksum evaluation flags (`-rltz`, `--checksum`, `--no-perms`, `--omit-dir-times`) to prevent inode timestamp updates during rsync pulls, while introducing a native post-pull sanitisation function that removes iOS-injected carriage returns without altering file modification times. +""" + +import argparse +import os +import subprocess +import time +from pathlib import Path +from threading import Timer +from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer + +# PRODUCTION CONFIGURATION +local_dir = "//home/sgbv/obsidian/" +remote_dest = "ish:/root/obsidian/" +debounce_seconds = 2.0 + +# ignore workspace ui states to prevent infinite sync loops +excludes = [ + "--exclude=.obsidian/workspace*", + "--exclude=.trash/", + "--exclude=.git/" +] + +def build_rsync_cmd(src, dest): + # use checksum and strip posix metadata mapping to prevent inode ctime destruction + flags = [ + "rsync", + "-rltz", + "--checksum", + "--delete", + "--no-perms", + "--no-owner", + "--no-group", + "--omit-dir-times" + ] + return flags + excludes + [src, dest] + +def sanitize_line_endings(): + # strip dos carriage returns natively while preserving file modification timestamps + print("sanitising line endings...") + vault_path = Path(local_dir) + for file_path in vault_path.rglob("*.md"): + try: + content = file_path.read_bytes() + if b"\r\n" in content: + clean_content = content.replace(b"\r\n", b"\n") + # record original timestamps before writing + stat_data = file_path.stat() + file_path.write_bytes(clean_content) + # restore original access and modification timestamps + os.utime(file_path, (stat_data.st_atime, stat_data.st_mtime)) + print(f"cleaned carriage returns: {file_path.name}") + except Exception as error: + print(f"sanitisation error on {file_path.name}: {error}") + +def pull_sync(): + print("pulling changes from ish to arch...") + subprocess.run(build_rsync_cmd(remote_dest, local_dir)) + sanitize_line_endings() + print("pull complete.") + +def push_sync(): + print("pushing changes from arch to ish...") + subprocess.run(build_rsync_cmd(local_dir, remote_dest)) + print("push complete.") + +class watchdog_push_handler(FileSystemEventHandler): + def __init__(self): + self.sync_timer = None + + def on_any_event(self, event): + # ignore directory changes and workspace or hidden cache updates + if event.is_directory or any(x in event.src_path for x in [".obsidian", ".trash"]): + return + + print(f"local change detected: {event.event_type}") + if self.sync_timer: + self.sync_timer.cancel() + + self.sync_timer = Timer(debounce_seconds, push_sync) + self.sync_timer.start() + +def watch_mode(): + print(f"arch watchdog listening on {local_dir}") + handler = watchdog_push_handler() + observer = Observer() + observer.schedule(handler, local_dir, recursive=True) + observer.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("\nstopping watchdog...") + observer.stop() + observer.join() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="arch sync node") + parser.add_argument("mode", nargs="?", default="watch", choices=["watch", "push", "pull"]) + args = parser.parse_args() + + if args.mode == "pull": + pull_sync() + elif args.mode == "push": + push_sync() + else: + watch_mode() diff --git a/.local/bin/pac-size b/.local/bin/pac-size new file mode 100755 index 0000000..5cf5318 --- /dev/null +++ b/.local/bin/pac-size @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +pacman -Qi $(pactree -u "$1") | awk '/^Installed Size/ { + size=$4 + if ($5 == "KiB") size /= 1024 + if ($5 == "GiB") size *= 1024 + if ($5 == "B") size /= 1048576 + total += size +} END { + printf "Total ecosystem size: %.2f MiB\n", total +}' diff --git a/.local/bin/pick b/.local/bin/pick new file mode 100755 index 0000000..88668ed --- /dev/null +++ b/.local/bin/pick @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +# DEPENDENCY CHECK +# ensures required wayland utilities are present before execution +if ! command -v hyprpicker >/dev/null 2>&1; then + notify-send "error" "hyprpicker is not installed." + exit 1 +fi + +# EXECUTION AND NOTIFICATION +# captures the hex value and triggers a system notification +HEX_CODE=$(hyprpicker -a) +notify-send "colour selected" "hex code $HEX_CODE copied to clipboard." diff --git a/.local/bin/power b/.local/bin/power new file mode 100755 index 0000000..1526c70 --- /dev/null +++ b/.local/bin/power @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# MENU ENTRIES +# define the string values for the power options +lock="lock" +suspend="suspend" +hibernate="hibernate" +logout="log out" +reboot="reboot" +shutdown="shutdown" + +# USER INPUT +# pipe options into fuzzel and capture the string output +selected=$(printf "%s\n%s\n%s\n%s\n%s\n%s" "$lock" "$suspend" "$hibernate" "$logout" "$reboot" "$shutdown" | fuzzel --dmenu --prompt "power: ") + +# EXECUTION ROUTING +# match the captured string to the system daemon command +case "$selected" in + "$lock") + swaylock -f -c 000000 + ;; + "$suspend") + loginctl suspend + ;; + "$hibernate") + loginctl hibernate + ;; + "$logout") + loginctl terminate-session "$XDG_SESSION_ID" + ;; + "$reboot") + loginctl reboot + ;; + "$shutdown") + loginctl shutdown + ;; + *) + exit 0 + ;; +esac diff --git a/.local/bin/rate b/.local/bin/rate new file mode 100755 index 0000000..91c7088 --- /dev/null +++ b/.local/bin/rate @@ -0,0 +1,468 @@ +#!/usr/bin/env python3 +# AUTHOR: Daesorin +# CREATED: 2026-05-26 12:45 +0100 +# UPDATED: 2026-07-12 15:13 +0100 +""" +rate v0.6 +terminal multi-asset valuation engine. runs entirely on the python standard +library with zero external dependencies. supports specialized workflows for +forex, precious metals, energy futures, market indices, crypto, equities, and ETFs. +""" + +import json +import os +import re +import socket +import sys +import urllib.error +import urllib.parse +import urllib.request + +# FORCE IPV4 RESOLUTION +# prevent unreachable network errors on broken ipv6 internet gateways +_old_getaddrinfo = socket.getaddrinfo + + +def _ipv4_getaddrinfo(*args, **kwargs): + responses = _old_getaddrinfo(*args, **kwargs) + return [res for res in responses if res[0] == socket.AF_INET] + + +socket.getaddrinfo = _ipv4_getaddrinfo + +# TERMINAL COLOURS + + +def get_colours(): + if not sys.stdout.isatty() or os.environ.get("NO_COLOR"): + return { + "red": "", + "green": "", + "yellow": "", + "blue": "", + "cyan": "", + "nc": "", + } + return { + "red": "\033[1;31m", + "green": "\033[1;32m", + "yellow": "\033[1;33m", + "blue": "\033[1;34m", + "cyan": "\033[1;36m", + "nc": "\033[0m", + } + + +COL = get_colours() + +# ASSET MAPS +CRYPTO_MAP = { + "btc": "bitcoin", + "eth": "ethereum", + "usdt": "tether", + "bnb": "binancecoin", + "sol": "solana", + "xrp": "ripple", + "usdc": "usd-coin", + "ada": "cardano", + "doge": "dogecoin", + "ton": "the-open-network", + "trx": "tron", + "avax": "avalanche-2", + "shib": "shiba-inu", + "dot": "polkadot", + "link": "chainlink", +} + +METAL_MAP = { + "gold": "GC=F", + "silver": "SI=F", + "platinum": "PL=F", + "palladium": "PA=F", + "copper": "HG=F", +} + +ENERGY_MAP = { + "oil": "CL=F", + "wti": "CL=F", + "brent": "BZ=F", + "gas": "NG=F", + "natgas": "NG=F", + "heating": "HO=F", +} + +INDEX_MAP = { + "sp500": "^GSPC", + "s&p500": "^GSPC", + "nasdaq": "^IXIC", + "dow": "^DJI", + "dji": "^DJI", + "ftse": "^FTSE", + "nikkei": "^N225", + "dax": "^GDAXI", +} + +# FORMATTING UTILITIES + + +def fmt_num(val, decimals=2): + return f"{val:,.{decimals}f}" + + +def fmt_rate(val): + if val == 0: + return "0.00" + if abs(val) < 0.0001: + return f"{val:,.8f}".rstrip("0").rstrip(".") + if abs(val) < 1.0: + return f"{val:,.4f}" + return f"{val:,.2f}" + + +# CORE HTTP ENGINE + + +def fetch_json(url, params=None): + if params: + url = f"{url}?{urllib.parse.urlencode(params)}" + req = urllib.request.Request( + url, + headers={ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "Accept": "application/json", + }, + ) + try: + with urllib.request.urlopen(req, timeout=10) as response: + return json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as e: + print( + f"{COL['red']}error: api request failed with status {e.code}.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + except urllib.error.URLError: + print( + f"{COL['red']}error: network connection timed out or unreachable.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + except json.JSONDecodeError: + print( + f"{COL['red']}error: failed to parse remote json payload.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + + +def get_forex_rate(base, target): + base, target = base.upper(), target.upper() + if base == target: + return 1.0 + data = fetch_json(f"https://open.er-api.com/v6/latest/{base}") + rates = data.get("rates", {}) + if target not in rates: + print( + f"{COL['red']}error: currency target '{target}' unsupported.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + return float(rates[target]) + + +def fetch_yahoo_market_data(ticker): + encoded = urllib.parse.quote(ticker) + url = f"https://query2.finance.yahoo.com/v8/finance/chart/{encoded}" + data = fetch_json(url, {"interval": "1d", "range": "1d"}) + result = data.get("chart", {}).get("result") + if not result: + print( + f"{COL['red']}error: market asset '{ticker}' not found on yahoo finance.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + meta = result[0].get("meta", {}) + price = float(meta.get("regularMarketPrice", 0.0)) + prev_close = float( + meta.get("chartPreviousClose") + or meta.get("previousClose") + or price + ) + return { + "symbol": meta.get("symbol", ticker.upper()), + "name": ( + meta.get("shortName") or meta.get("longName") or ticker + ).strip(), + "price": price, + "currency": meta.get("currency", "USD").upper(), + "change_pct": ((price - prev_close) / prev_close * 100.0) if prev_close else 0.0, + } + + +# UTILITY PARSER FOR MARKET INPUTS + + +def parse_market_args(args, default_target="NGN"): + amount = 1.0 + target = default_target + if len(args) >= 2: + if re.match(r"^[0-9]+(\.[0-9]+)?$", args[1]): + amount = float(args[1]) + if len(args) >= 3: + target = args[2] + else: + target = args[1] + if len(args) >= 3 and re.match(r"^[0-9]+(\.[0-9]+)?$", args[2]): + amount = float(args[2]) + return amount, target.upper() + + +# SUBCOMMAND EXECUTORS + + +def cmd_forex(args): + amount, base, target = 1.0, "USD", "NGN" + if len(args) == 1: + if re.match(r"^[0-9]+(\.[0-9]+)?$", args[0]): + amount = float(args[0]) + else: + base = args[0] + elif len(args) == 2: + if re.match(r"^[0-9]+(\.[0-9]+)?$", args[0]): + amount, base = float(args[0]), args[1] + else: + base, target = args[0], args[1] + elif len(args) >= 3: + if re.match(r"^[0-9]+(\.[0-9]+)?$", args[0]): + amount, base, target = float(args[0]), args[1], args[2] + else: + base, target, amount = args[0], args[1], float(args[2]) + + base, target = base.upper(), target.upper() + print( + f"{COL['blue']}fetching exchange rate for {base}/{target}...{COL['nc']}", + file=sys.stderr, + ) + rate = get_forex_rate(base, target) + val = amount * rate + print("-----------------------------------") + print(f"pair: {base} / {target}") + print(f"rate: {COL['yellow']}1 {base} = {fmt_rate(rate)} {target}{COL['nc']}") + print( + f"value: {COL['green']}{target} {fmt_num(val, 2)}{COL['nc']} ({base} {fmt_num(amount, 2)})" + ) + print("-----------------------------------") + + +def cmd_yahoo_generic(ticker, amount, target_curr, class_label): + print( + f"{COL['blue']}fetching evaluation for {class_label} [{ticker}]...{COL['nc']}", + file=sys.stderr, + ) + market = fetch_yahoo_market_data(ticker) + native_curr = market["currency"] + native_price = market["price"] + total_native = amount * native_price + + if native_curr != target_curr: + fx_rate = get_forex_rate(native_curr, target_curr) + final_val = total_native * fx_rate + rate_disp = f"{fmt_rate(native_price)} {native_curr} (~{fmt_num(native_price * fx_rate, 2)} {target_curr})" + val_disp = f"{COL['green']}{fmt_num(final_val, 2)} {target_curr}{COL['nc']} ({fmt_num(total_native, 2)} {native_curr})" + else: + rate_disp = f"{fmt_rate(native_price)} {native_curr}" + val_disp = f"{COL['green']}{fmt_num(total_native, 2)} {native_curr}{COL['nc']}" + + chg = market["change_pct"] + chg_col = COL["green"] if chg >= 0 else COL["red"] + chg_sign = "+" if chg >= 0 else "" + + print("-----------------------------------") + print(f"asset: {market['name']} ({market['symbol']})") + print(f"valuation: {COL['yellow']}{rate_disp}{COL['nc']}") + print(f"24h move: {chg_col}{chg_sign}{chg:.2f}%{COL['nc']}") + if amount != 1.0 or native_curr != target_curr: + print(f"total sum: {val_disp}") + print("-----------------------------------") + + +def cmd_metal(args): + if not args: + print( + f"{COL['red']}error: missing metal identifier.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + query = args[0].lower().strip() + ticker = METAL_MAP.get(query, args[0].upper()) + amount, target = parse_market_args(args) + cmd_yahoo_generic(ticker, amount, target, "Precious Metal") + + +def cmd_energy(args): + if not args: + print( + f"{COL['red']}error: missing energy identifier.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + query = args[0].lower().strip() + ticker = ENERGY_MAP.get(query, args[0].upper()) + amount, target = parse_market_args(args) + cmd_yahoo_generic(ticker, amount, target, "Energy Future") + + +def cmd_indices(args): + if not args: + print( + f"{COL['red']}error: missing index symbol.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + query = args[0].lower().strip() + ticker = INDEX_MAP.get(query, args[0].upper()) + amount, target = parse_market_args(args) + cmd_yahoo_generic(ticker, amount, target, "Market Index") + + +def cmd_stock(args): + if not args: + print( + f"{COL['red']}error: missing stock ticker.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + ticker = args[0].upper() + amount, target = parse_market_args(args) + cmd_yahoo_generic(ticker, amount, target, "Equity Asset") + + +def cmd_etf(args): + if not args: + print( + f"{COL['red']}error: missing ETF symbol.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + ticker = args[0].upper() + amount, target = parse_market_args(args) + cmd_yahoo_generic(ticker, amount, target, "Exchange Traded Fund") + + +def cmd_crypto(args): + if not args: + print( + f"{COL['red']}error: missing crypto token name or symbol.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + query = args[0].lower().strip() + amount, vs_curr = parse_market_args(args, default_target="ngn") + vs_target = vs_curr.lower() + + if query in CRYPTO_MAP: + coin_id, symbol = CRYPTO_MAP[query], query.upper() + else: + print( + f"{COL['blue']}searching coingecko for '{query}'...{COL['nc']}", + file=sys.stderr, + ) + search = fetch_json( + "https://api.coingecko.com/api/v3/search", {"query": query} + ) + coins = search.get("coins", []) + if not coins: + print( + f"{COL['red']}error: token '{query}' not found.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + coin_id, symbol = coins[0]["id"], coins[0]["symbol"].upper() + + print( + f"{COL['blue']}fetching {symbol} spot data in {vs_curr.upper()}...{COL['nc']}", + file=sys.stderr, + ) + data = fetch_json( + "https://api.coingecko.com/api/v3/simple/price", + { + "ids": coin_id, + "vs_currencies": vs_target, + "include_24hr_change": "true", + }, + ) + coin_data = data.get(coin_id, {}) + if vs_target not in coin_data: + print( + f"{COL['red']}error: target market currency '{vs_curr}' unsupported.{COL['nc']}", + file=sys.stderr, + ) + sys.exit(1) + + rate = float(coin_data[vs_target]) + change = float(coin_data.get(f"{vs_target}_24h_change", 0.0)) + val = amount * rate + chg_col = COL["green"] if change >= 0 else COL["red"] + chg_sign = "+" if change >= 0 else "" + + print("-----------------------------------") + print(f"crypto: {symbol} ({coin_id})") + print(f"spot rate: {COL['yellow']}{fmt_rate(rate)} {vs_curr.upper()}{COL['nc']}") + print(f"24h move: {chg_col}{chg_sign}{change:.2f}%{COL['nc']}") + if amount != 1.0: + print( + f"valuation: {COL['green']}{fmt_num(val, 2)} {vs_curr.upper()}{COL['nc']} ({symbol} {fmt_num(amount, 4)})" + ) + print("-----------------------------------") + + +# ROUTING MATRIX + + +def show_help(): + print("rate - multi-asset evaluation utility") + print("usage:") + print(" rate forex [amt] [target] fiat configurations (alias: fx)") + print(" rate metal precious spots contracts (alias: met)") + print(" rate energy fuel commodity options (alias: nrg)") + print(" rate indices global market indices (alias: ind)") + print(" rate crypto [amt] [curr] digital tokens valuations (alias: c)") + print(" rate stock [amt] [curr] corporate equities (alias: s)") + print(" rate etf [amt] [curr] exchange traded funds (alias: e)") + sys.exit(0) + + +def main(): + args = sys.argv[1:] + if not args or args[0] in ("-h", "--help", "help"): + show_help() + + cmd = args[0].lower() + sub_args = args[1:] + + if cmd in ("forex", "fx", "to", "from"): + if cmd == "to": + cmd_forex(["1", "NGN", sub_args[0]] if sub_args else ["NGN"]) + elif cmd == "from": + cmd_forex(["1", sub_args[0], "NGN"] if sub_args else ["USD", "NGN"]) + else: + cmd_forex(sub_args) + elif cmd in ("metal", "met"): + cmd_metal(sub_args) + elif cmd in ("energy", "nrg", "eng"): + cmd_energy(sub_args) + elif cmd in ("indices", "ind", "idx"): + cmd_indices(sub_args) + elif cmd in ("crypto", "c"): + cmd_crypto(sub_args) + elif cmd in ("stock", "stocks", "s"): + cmd_stock(sub_args) + elif cmd in ("etf", "e"): + cmd_etf(sub_args) + else: + # direct default fallback to legacy raw strings evaluation + cmd_forex(args) + + +if __name__ == "__main__": + main() diff --git a/.local/bin/recon b/.local/bin/recon new file mode 100755 index 0000000..030ec1d --- /dev/null +++ b/.local/bin/recon @@ -0,0 +1,189 @@ +#!/usr/bin/env bash + +set -o pipefail +export LC_ALL=C + +# COLOURS +# standard terminal output hues +red='\033[0;31m' +green='\033[0;32m' +yellow='\033[1;33m' +cyan='\033[0;36m' +nc='\033[0m' + +# CONFIGURATION +# defaults to mullvad dns for privacy-respecting resolution +resolver="@194.242.2.2" +timeout_val=2 +ua="mozilla/5.0 (x11; linux x86_64) applewebkit/537.36" + +# USAGE GUIDE +# displays instructions if arguments are missing +usage() { + printf "${cyan}usage:${nc} reconctl [-t timeout] \n" + printf " -t set network timeout in seconds (default: 2)\n" + printf " -h show help menu\n\n" + exit 1 +} + +while getopts "t:h" opt; do + case ${opt} in + t) timeout_val=$OPTARG ;; + h) usage ;; + *) usage ;; + esac +done +shift $((OPTIND - 1)) + +domain="$1" +[[ -z "$domain" ]] && usage + +# INPUT VALIDATION +# rejects malformed hostnames before they reach dig/curl/openssl/whois +if [[ ! "$domain" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$ ]]; then + printf "${red}error: '%s' is not a valid domain${nc}\n" "$domain" + exit 1 +fi + +# HELPER FUNCTIONS +# standardises section headers for clean output +section_header() { + printf "\n${cyan}%s${nc}\n" "$1" +} + +# DEPENDENCY AUDIT +# validates required packages before execution +for cmd in dig curl jq openssl whois awk; do + if ! command -v "$cmd" >/dev/null 2>&1; then + printf "${red}error: missing dependency '%s'${nc}\n" "$cmd" + exit 1 + fi +done + +# DNS ANALYSIS +# extracts primary records and security configurations +section_header "DNS RECORDS" +ipv4=$(dig $resolver +short A "$domain") +ipv6=$(dig $resolver +short AAAA "$domain") +cname=$(dig $resolver +short CNAME "$domain") +mx=$(dig $resolver +short MX "$domain") +caa=$(dig $resolver +short CAA "$domain") +txt=$(dig $resolver +short TXT "$domain" | grep -Ei "spf|dmarc" || true) +dnskey=$(dig $resolver +short DNSKEY "$domain") + +printf " ipv4:\n%s\n" "$([[ -n "$ipv4" ]] && echo "$ipv4" | sed 's/^/ /' || echo " n/a")" +printf " ipv6:\n%s\n" "$([[ -n "$ipv6" ]] && echo "$ipv6" | sed 's/^/ /' || echo " n/a")" +printf " cname:\n %s\n" "${cname:-none detected}" +printf " nameservers:\n%s\n" "$(dig $resolver +short NS "$domain" | sed 's/^/ /')" +printf " mail exchangers:\n%s\n" "$([[ -n "$mx" ]] && echo "$mx" | sed 's/^/ /' || echo " none detected")" +printf " caa records:\n%s\n" "$([[ -n "$caa" ]] && echo "$caa" | sed 's/^/ /' || echo " none detected")" +printf " security txt:\n%s\n" "$([[ -n "$txt" ]] && echo "$txt" | sed 's/^/ /' || echo " none detected")" +printf " dnssec: %s\n" "$([[ -n "$dnskey" ]] && echo "enabled" || echo "not detected")" + +# REVERSE LOOKUP +# checks ptr records for resolved v4 and v6 addresses +section_header "REVERSE DNS" +if [[ -n "$ipv4" || -n "$ipv6" ]]; then + for ip in $ipv4 $ipv6; do + ptr=$(dig $resolver +short -x "$ip") + printf " %s -> %s\n" "$ip" "${ptr:-n/a}" + done +else + printf " n/a\n" +fi + +# HTTP PERFORMANCE +# tests routing responses and redirection chains +section_header "HTTP ROUTING" +http_status=$(timeout "$timeout_val" curl -A "$ua" -sL -o /dev/null -w "%{http_code}" "http://$domain" || echo "timeout") +https_status=$(timeout "$timeout_val" curl -A "$ua" -sL -o /dev/null -w "%{http_code}" "https://$domain" 2>/dev/null || echo "timeout") +redirects=$(timeout "$timeout_val" curl -A "$ua" -sI -L "http://$domain" 2>/dev/null | grep -i "^location:" | sed 's/[Ll]ocation: //g' | tr -d '\r') + +printf " http status: %s\n" "$http_status" +printf " https status: %s\n" "$https_status" +printf " redirects:\n%s\n" "$([[ -n "$redirects" ]] && echo "$redirects" | sed 's/^/ -> /' || echo " none")" + +# ENCRYPTION AUDIT +# pulls subject, issuer, and expiration from the tls certificate +section_header "TLS CERTIFICATE" +tls=$(timeout "$timeout_val" openssl s_client -connect "$domain:443" -servername "$domain" -showcerts /dev/null) +expiry=$(echo "$tls" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) +subject_issuer=$(echo "$tls" | grep -E "subject=|issuer=" | sed 's/^[ \t]*//') +printf " subject/issuer:\n%s\n" "$([[ -n "$subject_issuer" ]] && echo "$subject_issuer" | sed 's/^/ /' || echo " unknown")" +printf " expiry date: %s\n" "${expiry:-unknown}" + +# SECURITY HEADERS +# evaluates presence of critical web security directives +section_header "SECURITY POSTURE" +headers=$(timeout "$timeout_val" curl -A "$ua" -sI "https://$domain" 2>/dev/null | tr -d '\r') +for h in "strict-transport-security" "content-security-policy" "x-frame-options" "x-content-type-options"; do + val=$(echo "$headers" | grep -i "^$h:" | cut -d: -f2- | xargs) + if [[ -n "$val" ]]; then + printf " %-25s ${green}present${nc}\n" "$h" + else + printf " %-25s ${yellow}missing${nc}\n" "$h" + fi +done + +# INFRASTRUCTURE PROBE +# checks common critical ports on the primary resolved ip, results sorted after concurrent scan +section_header "PORT SCAN" +ports=(21 22 25 53 80 443 3306 8080 8443) +target_ip=$(echo "$ipv4" | head -n1) + +if [[ -n "$target_ip" ]]; then + scan_tmp=$(mktemp) + exec 3>&2 + exec 2>/dev/null + for port in "${ports[@]}"; do + (timeout 1 bash -c "/dev/null && echo "$port" >> "$scan_tmp") & + done + wait + exec 2>&3 + if [[ -s "$scan_tmp" ]]; then + sort -n "$scan_tmp" | while read -r port; do + printf " port %-5s ${green}open${nc}\n" "$port" + done + else + printf " no open ports detected\n" + fi + rm -f "$scan_tmp" +else + printf " ${red}no ipv4 address for scanning.${nc}\n" +fi + +# ASSET ENUMERATION +# queries certificate transparency logs for child infrastructure via crt.sh json endpoint +section_header "SUBDOMAINS" +subdomains=$(timeout 15 curl -s "https://crt.sh/?q=%25.$domain&output=json" | jq -r '.[].name_value' 2>/dev/null | tr ',' '\n' | sed 's/\*\.//' | sort -u | head -n 15) +[[ -n "$subdomains" ]] && echo "$subdomains" | sed 's/^/ /' || echo " enumeration failed or none found" + +# REGISTRY AUDIT +# extracts baseline domain registration metadata +section_header "WHOIS DATA" +whois_data=$(timeout "$timeout_val" whois "$domain" 2>/dev/null | grep -Ei "^registrar:|^creation date:|^updated date:" | tr -s ' ' | head -n 5) +[[ -n "$whois_data" ]] && echo "$whois_data" | sed 's/^/ /' || echo " extraction limited" + +# RISK SCORE +# calculates a basic hazard rating based on missing protections and open ports +section_header "RISK SUMMARY" +risk=0 +[[ ! "$https_status" =~ ^(2|3) ]] && ((risk += 2)) +echo "$headers" | grep -qi "^strict-transport-security:" || ((risk += 1)) +echo "$headers" | grep -qi "^content-security-policy:" || ((risk += 1)) +[[ -z "$dnskey" ]] && ((risk += 1)) +timeout 1 bash -c "/dev/null && ((risk += 1)) +timeout 1 bash -c "/dev/null && ((risk += 2)) + +if [ $risk -le 1 ]; then + posture_color=$green + posture_text="low risk" +else + posture_color=$red + posture_text="risk identified" +fi + +printf " domain: %s\n" "$domain" +printf " ip: %s\n" "${target_ip:-n/a}" +printf " posture: ${posture_color}%s${nc} (score: %d)\n\n" "$posture_text" "$risk" + diff --git a/.local/bin/restore-brightness b/.local/bin/restore-brightness new file mode 100755 index 0000000..a3c9bb8 --- /dev/null +++ b/.local/bin/restore-brightness @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# AUTHOR: Daesorin +# CREATED: 2026-08-08 13:04 +0100 + +set -euo pipefail + +main() { + sleep 1 + brightnessctl set 10%} +} +main "$@" diff --git a/.local/bin/scratch b/.local/bin/scratch new file mode 100755 index 0000000..0ec6265 --- /dev/null +++ b/.local/bin/scratch @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# ~/.local/bin/scratchpadctl +# MANAGE SPATIAL SCRATCHPAD STATE + +APP_ID="$1" +SPAWN_CMD="$2" +HIDDEN_WORKSPACE="scratchpad_vault" + +WINDOW_JSON=$(niri msg -j windows | jq -c ".[] | select(.app_id == \"$APP_ID\")" | head -n 1) + +if [ -z "$WINDOW_JSON" ]; then + eval "$SPAWN_CMD" & + exit 0 +fi + +# extract geometric identifiers +WINDOW_ID=$(echo "$WINDOW_JSON" | jq -r '.id') +WINDOW_WS=$(echo "$WINDOW_JSON" | jq -r '.workspace_id') + +# query active operational space +CURRENT_WS=$(niri msg -j workspaces | jq -r '.[] | select(.is_focused) | .id') + +if [ "$WINDOW_WS" == "$CURRENT_WS" ]; then + niri msg action focus-window --id "$WINDOW_ID" + niri msg action move-window-to-workspace "$HIDDEN_WORKSPACE" +else + niri msg action focus-window --id "$WINDOW_ID" + niri msg action move-window-to-workspace "$CURRENT_WS" + niri msg action focus-window --id "$WINDOW_ID" +fi diff --git a/.local/bin/start-audio b/.local/bin/start-audio new file mode 100755 index 0000000..2777524 --- /dev/null +++ b/.local/bin/start-audio @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# AUTHOR: Daesorin +# CREATED: 2026-08-04 21:37 +0100 + +set -euo pipefail + +# TERMINATE EXISTING PROCESSES +# ensure a clean state before starting +killall -q wireplumber pipewire-pulse pipewire mpd +sleep 1 + +# INITIALISE AUDIO DAEMONS +# start the core pipewire service +pipewire & +sleep 1 + +# start the session manager +wireplumber & + +# start the pulseaudio compatibility layer +pipewire-pulse & + +# START MUSIC PLAYER DAEMON +# launch the background daemon for local music +mpd & + + diff --git a/.local/bin/status b/.local/bin/status new file mode 100755 index 0000000..974af38 --- /dev/null +++ b/.local/bin/status @@ -0,0 +1,365 @@ +#!/usr/bin/env bash +set -o pipefail +export LC_ALL=C + +# COLOURS +# define standard terminal output hues +green='\033[1;32m' +cyan='\033[1;36m' +yellow='\033[1;33m' +red='\033[1;31m' +nc='\033[0m' + +# HELPER FUNCTIONS +# standardises section headers and validates executable presence +section_header() { + printf "\n${green}%s${nc}\n" "$1" +} + +cmd_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# SYSTEM CORE +# prints basic kernel and hardware identity +print_system_core() { + local kernel cpu uptime_raw load_avg + kernel=$(uname -r) + cpu=$(grep "model name" /proc/cpuinfo | head -1 | cut -d':' -f2 | xargs) + uptime_raw=$(uptime -p | sed 's/up //') + load_avg=$(awk '{print $1, $2, $3}' /proc/loadavg) + + section_header "SYSTEM CORE" + printf " kernel: %s\n" "$kernel" + printf " cpu: %s\n" "$cpu" + printf " load: %s\n" "$load_avg" + printf " uptime: %s\n" "$uptime_raw" +} + +# GRAPHICS +# detects the display protocol and active compositor +print_graphics() { + local wm_name session_type + session_type="${XDG_SESSION_TYPE:-unknown}" + + wm_name="unknown" + if [ "$session_type" = "wayland" ]; then + wm_name="${XDG_CURRENT_DESKTOP:-Wayland}" + elif cmd_exists xprop; then + wm_name=$(xprop -root _NET_WM_NAME 2>/dev/null | cut -d'"' -f2) + [ -z "$wm_name" ] && wm_name=$(xprop -root WM_NAME 2>/dev/null | cut -d'"' -f2) + fi + + section_header "GRAPHICS" + printf " session: %s\n" "$session_type" + printf " compositor: %s\n" "$wm_name" + + if cmd_exists lspci; then + local gpus + gpus=$(lspci -mm | awk -F'"' -v OFS='' ' + tolower($2) ~ /^(vga compatible controller|3d controller|display controller)$/ {print $4}' | paste -sd ', ' -) + printf " gpu: %s\n" "${gpus:-unknown}" + else + printf " gpu: (lspci missing)\n" + fi +} + +# HARDWARE +# polls thermal sensors, power supply, and drive health +print_hardware() { + section_header "HARDWARE" + + local temp="" + if cmd_exists sensors; then + if cmd_exists jq; then + temp=$(sensors -j 2>/dev/null | jq -r '.. | ."Package id 0"? // ."Tctl"? // empty | .temp1_input' 2>/dev/null) + if [ -n "$temp" ]; then + temp=$(printf "%.1f°C" "$temp") + fi + fi + if [ -z "$temp" ]; then + temp=$(sensors 2>/dev/null | grep -m1 -E 'Package id 0|Tctl|Tdie' | awk '{print $4}' | sed 's/+//;s/°C//') + [ -n "$temp" ] && temp="${temp}°C" + fi + [ -z "$temp" ] && temp=$(sensors 2>/dev/null | grep -m1 'temp1' | awk '{print $2}' | sed 's/+//') + else + for zone in /sys/class/thermal/thermal_zone*; do + local type temp_val + type=$(cat "$zone/type" 2>/dev/null) + temp_val=$(cat "$zone/temp" 2>/dev/null) + if [ -n "$temp_val" ]; then + temp="$((temp_val/1000))°C ($type)" + break + fi + done + fi + printf " temp: %s\n" "${temp:-unknown}" + + local bat_info="" + for bat in /sys/class/power_supply/BAT*; do + if [ -d "$bat" ]; then + local level status name + name=$(basename "$bat") + level=$(cat "$bat/capacity" 2>/dev/null || echo "?") + status=$(cat "$bat/status" 2>/dev/null || echo "unknown") + bat_info="${bat_info}${name}: ${level}% (${status}) " + fi + done + if [ -n "$bat_info" ]; then + printf " battery: %s\n" "$(echo "$bat_info" | xargs)" + fi + + local smart_health="" + if cmd_exists nvme && [ -e /dev/nvme0 ]; then + smart_health=$(nvme smart-log /dev/nvme0 2>/dev/null | awk '/percentage_used/ {print $3"%" used}') + elif cmd_exists smartctl; then + smart_health=$(smartctl -H /dev/sda 2>/dev/null | awk '/SMART overall-health|SMART Health Status/ {print $NF}') + fi + printf " disk health: %s\n" "${smart_health:-not checked}" +} + +# RESOURCES +# calculates memory pressure and storage consumption +print_resources() { + local mem_data total used avail mem_pct swap_data s_total s_used + mem_data=$(free -m | awk '/^Mem:/ {print $2,$3,$7}') + read -r total used avail <<<"$mem_data" + mem_pct=$((100 * used / total)) + + swap_data=$(free -m | awk '/^Swap:/ {print $2,$3}') + read -r s_total s_used <<<"$swap_data" + + section_header "RESOURCES" + printf " ram: %sMi / %sMi (%s%% used)\n" "$used" "$total" "$mem_pct" + printf " avail: %sMi reclaimable\n" "$avail" + printf " swap: %sMi / %sMi used\n" "$s_used" "$s_total" + printf " disk: %s\n" "$(df -h / | awk 'NR==2 {print $3 " / " $2 " (" $5 " used)"}')" + + if [ -d /var/cache/pacman/pkg ]; then + local cache_size + cache_size=$(du -sh /var/cache/pacman/pkg 2>/dev/null | cut -f1) + printf " pkg cache: %s\n" "${cache_size:-?}" + else + printf " pkg cache: (missing)\n" + fi +} + +# NETWORK +# outputs ipv4 addressing and routing states +print_network() { + section_header "NETWORK" + + ip -4 addr show | grep "inet " | grep -v "127.0.0.1" | \ + awk '{print " " $NF ": " $2}' | sed 's|/.*||' + + local gateway + gateway=$(ip route | grep default | awk '{print $3}' | head -n1) + printf " gateway: %s\n" "$gateway" + + local dns + dns=$(grep nameserver /etc/resolv.conf 2>/dev/null | awk '{print $2}' | head -n2 | xargs) + [ -z "$dns" ] && dns="(none)" + printf " dns: %s\n" "$dns" + + for iface in /sys/class/net/*; do + if [ "$(cat "$iface/operstate" 2>/dev/null)" = "up" ]; then + local iface_name + iface_name=$(basename "$iface") + local speed + speed=$(cat "$iface/speed" 2>/dev/null) + [ -n "$speed" ] && printf " %s speed: %s Mbit/s\n" "$iface_name" "$speed" + + if cmd_exists iw; then + local ssid + ssid=$(iw dev "$iface_name" link 2>/dev/null | awk '/SSID/ {print $2}') + [ -n "$ssid" ] && printf " %s ssid: %s\n" "$iface_name" "$ssid" + fi + fi + done + + if cmd_exists curl; then + local ext_ip + ext_ip=$(curl -s --max-time 2 ifconfig.me 2>/dev/null) + [ -n "$ext_ip" ] && printf " external: %s\n" "$ext_ip" + fi +} + +# PACKAGES +# queries the local pacman database for state and updates +print_packages() { + if ! cmd_exists pacman; then + return + fi + + local pkg_total pkg_explicit pkg_aur pkg_updates pkg_orphan + pkg_total=$(pacman -Qq 2>/dev/null | wc -l) + pkg_explicit=$(pacman -Qe 2>/dev/null | wc -l) + pkg_aur=$(pacman -Qm 2>/dev/null | wc -l) + pkg_orphan=$(pacman -Qtdq 2>/dev/null | wc -l) + + section_header "PACKAGES" + printf " total: %s\n" "$pkg_total" + printf " explicit: %s\n" "$pkg_explicit" + printf " aur: %s\n" "$pkg_aur" + printf " orphans: %s\n" "$pkg_orphan" + + if cmd_exists checkupdates; then + pkg_updates=$(checkupdates 2>/dev/null | wc -l) + if [ "$pkg_updates" -gt 0 ]; then + printf " updates: ${yellow}%s pending${nc}\n" "$pkg_updates" + else + printf " updates: up to date\n" + fi + else + printf " updates: (checkupdates not installed)\n" + fi + + local last_sync + last_sync=$(grep -i "full system upgrade" /var/log/pacman.log 2>/dev/null | tail -1 | awk '{print $1, $2}' | sed 's/\[//;s/\]//;s/ \[PACMAN\]//') + printf " last sync: %s\n" "${last_sync:-unknown}" + + if grep -q '^\[testing\]' /etc/pacman.conf 2>/dev/null; then + printf " testing: ${yellow}enabled${nc}\n" + else + printf " testing: disabled\n" + fi +} + +# SECURITY +# checks kernel hardening and audits package vulnerabilities +print_security() { + section_header "SECURITY" + + local kernel_type + kernel_type=$(uname -r | grep -qi "hardened" && echo "hardened" || echo "standard") + printf " kernel: %s\n" "$kernel_type" + + if cmd_exists arch-audit; then + local vulns + vulns=$(arch-audit -q 2>/dev/null | wc -l) + if [ "$vulns" -gt 0 ]; then + printf " vulns: ${red}%d packages${nc}\n" "$vulns" + else + printf " vulns: none\n" + fi + fi +} + +# PERFORMANCE +# reports on cpu scaling and underlying disk scheduling +print_performance() { + section_header "PERFORMANCE" + + if [ -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor ]; then + local gov freq + gov=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor) + printf " governor: %s\n" "$gov" + if [ -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq ]; then + freq=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq) + printf " cpu freq: %s MHz\n" $((freq / 1000)) + fi + else + printf " governor: not available\n" + fi + + local root_dev + root_dev=$(findmnt -n -o SOURCE / 2>/dev/null) + if [ -n "$root_dev" ]; then + local disk_name + disk_name=$(lsblk -ndo PKNAME "$root_dev" 2>/dev/null) + [ -z "$disk_name" ] && disk_name=$(lsblk -ndo NAME "$root_dev" 2>/dev/null) + if [ -n "$disk_name" ] && [ -f "/sys/block/$disk_name/queue/scheduler" ]; then + local scheduler + scheduler=$(grep -oP '\[\K[^\]]+' "/sys/block/$disk_name/queue/scheduler") + printf " scheduler: %s\n" "${scheduler:-unknown}" + else + printf " scheduler: (no sysfs entry)\n" + fi + else + printf " scheduler: (root device unknown)\n" + fi +} + +# SYSTEMD HEALTH +# audits systemd for failed services and boot times +print_systemd_health() { + if ! cmd_exists systemctl; then + return + fi + + section_header "SYSTEMD HEALTH" + + local failed_units + failed_units=$(systemctl --failed --no-legend 2>/dev/null | wc -l) + if [ "$failed_units" -gt 0 ]; then + printf " failed units: ${red}%d${nc}\n" "$failed_units" + systemctl --failed --no-legend 2>/dev/null | awk '{print " " $1}' + else + printf " failed units: none\n" + fi + + if cmd_exists journalctl; then + local boot_time + boot_time=$(journalctl --list-boots 2>/dev/null | tail -1 | awk '{print $4}') + [ -n "$boot_time" ] && printf " boot time: %s\n" "$boot_time" + fi +} + +# CONTAINERS AND VIRTUALISATION +# detects running instances across incus, docker, and libvirt +print_containers_virt() { + section_header "CONTAINERS & VIRTUALISATION" + + if cmd_exists incus; then + local incus_count + incus_count=$(incus list status=running -c n --format csv 2>/dev/null | wc -l) + printf " incus: %d active instances\n" "$incus_count" + fi + + if cmd_exists docker && systemctl -q is-active docker 2>/dev/null; then + local containers + containers=$(docker ps -q 2>/dev/null | wc -l) + printf " docker: %d running containers\n" "$containers" + fi + + if cmd_exists podman && systemctl -q is-active podman 2>/dev/null; then + local pods + pods=$(podman ps -q 2>/dev/null | wc -l) + printf " podman: %d running containers\n" "$pods" + fi + + if cmd_exists virsh && systemctl -q is-active libvirtd 2>/dev/null; then + local vms + vms=$(virsh list --name 2>/dev/null | grep -v "^$" | wc -l) + printf " libvirt: %d active VMs\n" "$vms" + fi +} + +# TIMERS +# counts active systemd chron tasks +print_timers() { + if ! cmd_exists systemctl; then + return + fi + + local timer_count + timer_count=$(systemctl list-timers --no-legend 2>/dev/null | wc -l) + if [ "$timer_count" -gt 0 ]; then + section_header "TIMERS" + printf " active: %d systemd timers\n" "$timer_count" + fi +} + +# EXECUTION +print_system_core +print_graphics +print_hardware +print_resources +print_network +print_packages +print_security +print_performance +print_systemd_health +print_containers_virt +print_timers +printf "\n" diff --git a/.local/bin/strap-archlinux-vm b/.local/bin/strap-archlinux-vm new file mode 100755 index 0000000..529e20e --- /dev/null +++ b/.local/bin/strap-archlinux-vm @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# AUTHOR: Daesorin +# CREATED: 2026-08-03 18:35 +0100 + +set -euo pipefail + +main() { + incus init archlinux-vm --empty --vm -c security.secureboot=false + incus config device add archlinux-vm install-iso disk source=//home/sgbv/.iso/archlinux.iso boot.priority=10 + incus start archlinux-vm + incus console archlinux-vm --type=vga +} + +main "$@" diff --git a/.local/bin/strap-artix-vm b/.local/bin/strap-artix-vm new file mode 100755 index 0000000..d51ffa6 --- /dev/null +++ b/.local/bin/strap-artix-vm @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# AUTHOR +# daesorin +# CREATED +# 2026-08-03 18:35 +0100 + +set -euo pipefail + +main() { + aria2c --dir=//home/sgbv/.iso --seed-time=0 //home/sgbv/Downloads/artix-base-openrc-20260729-x86_64.iso.torrent + incus init artix-vm --empty --vm -c security.secureboot=false + incus config device add artix-vm install-iso disk source=//home/sgbv/.iso/artix-base-openrc-20260729-x86_64.iso boot.priority=10 + incus start artix-vm + incus console artix-vm --type=vga +} + +main "$@" diff --git a/.local/bin/volume-notify b/.local/bin/volume-notify new file mode 100755 index 0000000..7fcfce9 --- /dev/null +++ b/.local/bin/volume-notify @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# AUTHOR: Daesorin +# CREATED: 2026-08-04 05:01 +0100 + +set -euo pipefail + +if [ "$1" = "toggle" ]; then + pamixer -t +elif [[ "$1" == +* ]]; then + val="${1#+}" + pamixer -i "${val%\%}" +elif [[ "$1" == -* ]]; then + val="${1#-}" + pamixer -d "${val%\%}" +fi + +volume=$(pamixer --get-volume) +muted=$(pamixer --get-mute) + +if [ "$muted" = "true" ]; then + notify-send -h string:x-canonical-private-synchronous:volume -u low "Volume: Muted" +else + notify-send -h string:x-canonical-private-synchronous:volume -u low "Volume: ${volume}%" +fi diff --git a/.local/bin/waybar-net b/.local/bin/waybar-net new file mode 100755 index 0000000..b6bfeb2 --- /dev/null +++ b/.local/bin/waybar-net @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +if ! vnstat -i wlan0 --oneline >/dev/null 2>&1; then + echo "{\"text\": \"Offline\", \"tooltip\": \"vnstat daemon unavailable\"}" + exit 0 +fi + +raw_data=$(vnstat -i wlan0 --oneline) + +rx_today=$(echo "$raw_data" | cut -d';' -f4) +tx_today=$(echo "$raw_data" | cut -d';' -f5) +total_today=$(echo "$raw_data" | cut -d';' -f6) + +echo "{\"text\": \"↓ $rx_today ↑ $tx_today\", \"tooltip\": \"today total: $total_today\"}" diff --git a/.local/bin/weather b/.local/bin/weather new file mode 100755 index 0000000..1e43cf6 --- /dev/null +++ b/.local/bin/weather @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# AUTHOR: Daesorin +# CREATED: 2026-07-18 11:26 +0100 + +set -euo pipefail + +# DEFINE API ENDPOINT +# store the target url and authentication parameter in a shell variable +readonly API_URL="http://api.weatherapi.com/v1/current.json?key=d06994d0d97744ae89d172923261707&q=Idogbo&aqi=no" + +# FETCH AND PARSE WEATHER PAYLOAD +# curl fetches the data silently and pipes the stream directly into jq for extraction +curl -s "$API_URL" | jq -r ' + "Location: \(.location.name), \(.location.region), \(.location.country)", + "Local Time: \(.location.localtime)", + "Temperature: \(.current.temp_c)°C", + "Condition: \(.current.condition.text)" +' diff --git a/.zsh/.functions/cdc b/.zsh/.functions/cdc new file mode 100644 index 0000000..7c1df94 --- /dev/null +++ b/.zsh/.functions/cdc @@ -0,0 +1 @@ +cd "$1" && clear; diff --git a/.zsh/.functions/copy b/.zsh/.functions/copy new file mode 100644 index 0000000..4f28491 --- /dev/null +++ b/.zsh/.functions/copy @@ -0,0 +1,47 @@ +# PIPED INPUT HANDLING +# read directly from standard input when no arguments are provided +if [ ! -t 0 ] && [ $# -eq 0 ]; then + wl-copy + return 0 +fi + +# ARGUMENT VALIDATION +# ensure at least one file argument is passed when not piping +if [ $# -eq 0 ]; then + echo "usage: copy or | copy" >&2 + return 1 +fi + +# FILE EXISTENCE CHECK +# verify the target argument is a readable file +if [ ! -r "$1" ]; then + echo "error: cannot read file '$1'" >&2 + return 1 +fi + +# MIME TYPE DETECTION +# inspect the file header to determine the correct clipboard target +local mime_type +mime_type=$(file --mime-type -b "$1") + +# CLIPBOARD DISPATCH +# route the file to wl-copy with the appropriate target flags +case "$mime_type" in + image/png|image/jpeg|image/webp|image/gif|image/bmp) + wl-copy -t "$mime_type" < "$1" + echo "copied $1 ($mime_type) to clipboard." + ;; + image/svg+xml) + if command -v rsvg-convert >/dev/null 2>&1; then + rsvg-convert "$1" | wl-copy -t image/png + echo "rasterised $1 to png and copied to clipboard." + else + wl-copy -t image/svg+xml < "$1" + echo "copied $1 (raw svg) to clipboard. install librsvg for automatic png rasterisation." + fi + ;; + *) + cat "$@" | wl-copy + echo "copied text content ($# file(s)) to clipboard." + ;; +esac diff --git a/.zsh/.functions/git.abort b/.zsh/.functions/git.abort new file mode 100644 index 0000000..312acbf --- /dev/null +++ b/.zsh/.functions/git.abort @@ -0,0 +1,4 @@ +local branch +branch=$(git branch --show-current) +git fetch origin && +git reset --hard "origin/$branch" diff --git a/.zsh/.functions/git.fire b/.zsh/.functions/git.fire new file mode 100644 index 0000000..c35ca9c --- /dev/null +++ b/.zsh/.functions/git.fire @@ -0,0 +1,5 @@ +local branch="fire-$(date +%Y%m%d-%H%M)" +git checkout -b "$branch" && +git add -A && +git commit -m "FIRE EMERGENCY: $branch" --no-verify && +git push -u origin "$branch" diff --git a/.zsh/.functions/git.grab b/.zsh/.functions/git.grab new file mode 100644 index 0000000..25d7dd0 --- /dev/null +++ b/.zsh/.functions/git.grab @@ -0,0 +1 @@ +git checkout "$1" -- "$2" diff --git a/.zsh/.functions/git.lazy b/.zsh/.functions/git.lazy new file mode 100644 index 0000000..113d0c0 --- /dev/null +++ b/.zsh/.functions/git.lazy @@ -0,0 +1,8 @@ +if [ -z "$1" ]; then + echo "Usage: git.lazy \"commit message\"" + return 1 +fi + +git add -A && +git commit -m "$1" && +git push diff --git a/.zsh/.functions/git.nuke b/.zsh/.functions/git.nuke new file mode 100644 index 0000000..7b39d00 --- /dev/null +++ b/.zsh/.functions/git.nuke @@ -0,0 +1,7 @@ +if [ -z "$1" ]; then + echo "Usage: git.nuke " + return 1 +fi + +git branch -D "$1" +git push origin --delete "$1" diff --git a/.zsh/.functions/hl-bar b/.zsh/.functions/hl-bar new file mode 100644 index 0000000..7e19204 --- /dev/null +++ b/.zsh/.functions/hl-bar @@ -0,0 +1,11 @@ +# HISTOGRAM RENDERING +# render horizontal bar chart of account balances +local chart_script="$HOME/.local/bin/hledger-charts" +local journal="${HLEDGER_JOURNAL:-$HOME/obsidian/finances/journals/2026.journal}" + +if [[ -x "$chart_script" ]]; then + "$chart_script" histogram --journal "$journal" "${@:-expenses}" +else + echo "warning: hledger-charts not found, defaulting to native percentage breakdown" >&2 + hledger -f "$journal" balance "${@:-expenses}" --percent +fi diff --git a/.zsh/.functions/hl-debt b/.zsh/.functions/hl-debt new file mode 100644 index 0000000..d161559 --- /dev/null +++ b/.zsh/.functions/hl-debt @@ -0,0 +1,9 @@ +# DEBT AND LIABILITY TRACKING +# view loan balances over time using inverted multi-line sparklines +local chart_script="$HOME/.local/bin/hledger-charts" +local journal="${HLEDGER_JOURNAL:-$HOME/obsidian/finances/journals/2026.journal}" +if [[ -x "$chart_script" ]]; then + "$chart_script" trend --journal "$journal" -i -r 2 "${@:-liabilities}" +else + hledger -f "$journal" balance -M "${@:-liabilities}" --invert +fi diff --git a/.zsh/.functions/hl-group b/.zsh/.functions/hl-group new file mode 100644 index 0000000..7545f57 --- /dev/null +++ b/.zsh/.functions/hl-group @@ -0,0 +1,11 @@ +# GROUPED BAR RENDERING +# render monthly bar comparisons by account +local chart_script="$HOME/.local/bin/hledger-charts" +local journal="${HLEDGER_JOURNAL:-$HOME/obsidian/finances/journals/2026.journal}" + +if [[ -x "$chart_script" ]]; then + "$chart_script" grouped --journal "$journal" "${@:-expenses}" +else + echo "warning: hledger-charts not found, defaulting to monthly tabular matrix" >&2 + hledger -f "$journal" balance -M "${@:-expenses}" +fi diff --git a/.zsh/.functions/hl-inc b/.zsh/.functions/hl-inc new file mode 100644 index 0000000..bda767e --- /dev/null +++ b/.zsh/.functions/hl-inc @@ -0,0 +1,10 @@ +# INCOME DISTRIBUTION +# view earnings breakdown with inverted mathematical signs for positive display +local chart_script="$HOME/.local/bin/hledger-charts" +local journal="${HLEDGER_JOURNAL:-$HOME/obsidian/finances/journals/2026.journal}" + +if [[ -x "$chart_script" ]]; then + "$chart_script" pie --journal "$journal" -i "${@:-income}" +else + hledger -f "$journal" balance "${@:-income}" --invert +fi diff --git a/.zsh/.functions/hl-pie b/.zsh/.functions/hl-pie new file mode 100644 index 0000000..43fde23 --- /dev/null +++ b/.zsh/.functions/hl-pie @@ -0,0 +1,11 @@ +# PIE AND PROPORTIONAL RENDERING +# render proportional ascii bar and distribution chart +local chart_script="$HOME/.local/bin/hledger-charts" +local journal="${HLEDGER_JOURNAL:-$HOME/obsidian/finances/journals/2026.journal}" + +if [[ -x "$chart_script" ]]; then + "$chart_script" pie --journal "$journal" "${@:-expenses}" +else + echo "warning: hledger-charts not found, defaulting to native percentage breakdown" >&2 + hledger -f "$journal" balance "${@:-expenses}" --percent +fi diff --git a/.zsh/.functions/hl-stack b/.zsh/.functions/hl-stack new file mode 100644 index 0000000..0a9bda3 --- /dev/null +++ b/.zsh/.functions/hl-stack @@ -0,0 +1,11 @@ +# STACKED BAR RENDERING +# render stacked proportional bars across time periods +local chart_script="$HOME/.local/bin/hledger-charts" +local journal="${HLEDGER_JOURNAL:-$HOME/obsidian/finances/journals/2026.journal}" + +if [[ -x "$chart_script" ]]; then + "$chart_script" stacked --journal "$journal" "${@:-expenses}" +else + echo "warning: hledger-charts not found, defaulting to monthly tabular matrix" >&2 + hledger -f "$journal" balance -M "${@:-expenses}" +fi diff --git a/.zsh/.functions/hl-trend b/.zsh/.functions/hl-trend new file mode 100644 index 0000000..5eaaa16 --- /dev/null +++ b/.zsh/.functions/hl-trend @@ -0,0 +1,11 @@ +# TREND RENDERING +# render multi-period balance trends with unicode sparklines +local chart_script="$HOME/.local/bin/hledger-charts" +local journal="${HLEDGER_JOURNAL:-$HOME/obsidian/finances/journals/2026.journal}" + +if [[ -x "$chart_script" ]]; then + "$chart_script" trend --journal "$journal" "${@:-expenses}" +else + echo "warning: hledger-charts not found, defaulting to monthly tabular matrix" >&2 + hledger -f "$journal" balance -M "${@:-expenses}" +fi diff --git a/.zsh/.functions/hl-view b/.zsh/.functions/hl-view new file mode 100644 index 0000000..9af5897 --- /dev/null +++ b/.zsh/.functions/hl-view @@ -0,0 +1,14 @@ +# GRAPHICAL IMAGE RENDERING +# generate matplotlib png image and open immediately with imv +local subcommand="${1:-pie}" +shift 2>/dev/null || true +local chart_script="$HOME/.local/bin/hledger-charts" +local journal="${HLEDGER_JOURNAL:-$HOME/obsidian/finances/journals/2026.journal}" +local tmp_image="/tmp/hledger_chart.png" + +if [[ -x "$chart_script" ]]; then + "$chart_script" "$subcommand" --journal "$journal" --png "$tmp_image" --theme "${HL_THEME:-dark_background}" "${@:-expenses}" && imv "$tmp_image" +else + echo "error: hledger-charts binary not executable at $chart_script" >&2 + return 1 +fi diff --git a/.zsh/.functions/lanscan b/.zsh/.functions/lanscan new file mode 100644 index 0000000..9459579 --- /dev/null +++ b/.zsh/.functions/lanscan @@ -0,0 +1,11 @@ +local iface_info=$(ip -o -f inet addr show | awk '/scope global/ {print $2, $4}' | head -1) +local iface=$(echo $iface_info | awk '{print $1}') +local subnet=$(echo $iface_info | awk '{print $2}') + +if [ -z "$subnet" ]; then + echo "❌ No active network connection found." + return 1 +fi + +echo "🔍 Scanning subnet \033[33m$subnet\033[0m on \033[33m$iface\033[0m..." +doas nmap -sn -n "$subnet" | grep -E "Nmap scan|MAC Address" | sed -e 's/Nmap scan report for /IP: /g' -e 's/MAC Address: /MAC: /g' diff --git a/.zsh/.functions/mkcd b/.zsh/.functions/mkcd new file mode 100644 index 0000000..c56109f --- /dev/null +++ b/.zsh/.functions/mkcd @@ -0,0 +1 @@ +mkdir -p "$1" && cd "$1" diff --git a/.zsh/.functions/nbash b/.zsh/.functions/nbash new file mode 100644 index 0000000..2689ca7 --- /dev/null +++ b/.zsh/.functions/nbash @@ -0,0 +1,46 @@ + local target="${1:-}" + if [[ -z "$target" ]]; then + echo "usage: nbash " + return 1 + fi + + # PREVENT OVERWRITES + # exit immediately if the target file already exists + if [[ -f "$target" ]]; then + echo "error: file '$target' already exists." + return 1 + fi + + # CREATE DIRECTORIES + # ensure the parent directory structure exists before creating the file + mkdir -p "$(dirname "$target")" + + # TEMPLATE SELECTION + # use the external template if available, otherwise generate inline + local template="$HOME/.bash_templates/script.sh" + if [[ -f "$template" ]]; then + cp "$template" "$target" + else + cat > "$target" </dev/null) + [[ -n "$speed" && "$speed" != "-1" ]] && echo "├── $iface: ${speed} Mb/s" +done diff --git a/.zsh/.functions/npython b/.zsh/.functions/npython new file mode 100644 index 0000000..cae97e3 --- /dev/null +++ b/.zsh/.functions/npython @@ -0,0 +1,47 @@ + local target="${1:-}" + if [[ -z "$target" ]]; then + echo "usage: npython " + return 1 + fi + + # PREVENT OVERWRITES + # exit immediately if the target file already exists + if [[ -f "$target" ]]; then + echo "error: file '$target' already exists." + return 1 + fi + + # CREATE DIRECTORIES + # ensure the parent directory structure exists before creating the file + mkdir -p "$(dirname "$target")" + + # TEMPLATE SELECTION + # use the external template if available, otherwise generate inline + local template="$HOME/.python_templates/script.py" + if [[ -f "$template" ]]; then + cp "$template" "$target" + else + cat > "$target" </dev/null; then + echo "✅ $host: reachable" + else + echo "❌ $host: unreachable" + fi +done diff --git a/.zsh/.functions/pm-orphans b/.zsh/.functions/pm-orphans new file mode 100644 index 0000000..53b8110 --- /dev/null +++ b/.zsh/.functions/pm-orphans @@ -0,0 +1,6 @@ +orphans=$(pacman -Qtdq) +if [[ -n "$orphans" ]]; then + sudo pacman -Rns $orphans +else + printf "no orphan packages found.\n" +fi diff --git a/.zsh/.functions/portscan b/.zsh/.functions/portscan new file mode 100644 index 0000000..34e0f46 --- /dev/null +++ b/.zsh/.functions/portscan @@ -0,0 +1,3 @@ +[[ -z "$1" ]] && { echo "Usage: portscan "; return 1; } +nmap "$1" + diff --git a/.zsh/.functions/serve b/.zsh/.functions/serve new file mode 100644 index 0000000..8c2fe9d --- /dev/null +++ b/.zsh/.functions/serve @@ -0,0 +1,3 @@ +# LOCAL WEB SERVER +# host current directory on specified port or default to 8000 +python3 -m http.server "${1:-8000}" diff --git a/.zsh/.functions/speedtest b/.zsh/.functions/speedtest new file mode 100644 index 0000000..2f8f75a --- /dev/null +++ b/.zsh/.functions/speedtest @@ -0,0 +1,5 @@ +if command -v speedtest-cli &> /dev/null; then + speedtest-cli + else + echo "Install speedtest-cli: sudo pacman -S speedtest-cli" +fi diff --git a/.zsh/.functions/sshkeygen b/.zsh/.functions/sshkeygen new file mode 100644 index 0000000..648ce30 --- /dev/null +++ b/.zsh/.functions/sshkeygen @@ -0,0 +1,35 @@ +# SSH KEY GENERATOR +# creates an ed25519 keypair and copies the public key to the clipboard +local email=$1 +local keytype="ed25519" +local keyname=${2:-id_ed25519} + +if [[ -z "$email" ]]; then + echo "Usage: sshkeygen [keyname]" + return 1 +fi + +local ssh_dir="$HOME/.ssh" +mkdir -p "$ssh_dir" +chmod 700 "$ssh_dir" + +local keypath="$ssh_dir/$keyname" +if [[ -f "$keypath" ]]; then + echo "\033[31m❌ Key already exists at $keypath\033[0m" + return 1 +fi + +ssh-keygen -t "$keytype" -C "$email" -f "$keypath" -N "" +echo "\033[32m✅ SSH key created at $keypath\033[0m" + +# CLIPBOARD INTEGRATION +# auto-copy the public key based on the active display server +if command -v wl-copy &> /dev/null; then + cat "$keypath.pub" | wl-copy + echo "\033[36m📋 Public key copied to clipboard (Wayland)!\033[0m" +elif command -v xclip &> /dev/null; then + cat "$keypath.pub" | xclip -selection clipboard + echo "\033[36m📋 Public key copied to clipboard (X11)!\033[0m" +else + cat "$keypath.pub" +fi diff --git a/.zsh/.functions/sshkeylist b/.zsh/.functions/sshkeylist new file mode 100644 index 0000000..951389b --- /dev/null +++ b/.zsh/.functions/sshkeylist @@ -0,0 +1,4 @@ +# SSH KEY LISTER +# displays all public keys currently stored in the local ssh directory +echo "\033[36m📂 Existing SSH keys in ~/.ssh:\033[0m" +ls -lh "$HOME/.ssh/"*.pub 2>/dev/null || echo "No SSH keys found." diff --git a/.zsh/.functions/venv b/.zsh/.functions/venv new file mode 100644 index 0000000..a88ded6 --- /dev/null +++ b/.zsh/.functions/venv @@ -0,0 +1,18 @@ +# VIRTUAL ENVIRONMENT MANAGER +# creates a new environment if it does not exist, then activates it +local venv_dir="${1:-.venv}" + +if [[ ! -d "$venv_dir" ]]; then + echo "initialising new virtual environment in ./$venv_dir..." + python3 -m venv "$venv_dir" +fi + +# ACTIVATE ENVIRONMENT +# source the activation script into the current shell session +if [[ -f "$venv_dir/bin/activate" ]]; then + source "$venv_dir/bin/activate" + echo "activated $venv_dir" +else + echo "error: activation script not found in ./$venv_dir" + return 1 +fi diff --git a/.zsh/.zshrc b/.zsh/.zshrc new file mode 100644 index 0000000..5827b35 --- /dev/null +++ b/.zsh/.zshrc @@ -0,0 +1,7 @@ +[[ -o interactive ]] || return + +if [[ -d "$ZDOTDIR" ]]; then + for config in "$ZDOTDIR"/*.zsh(N); do + source "$config" + done +fi diff --git a/.zsh/00-umask.zsh b/.zsh/00-umask.zsh new file mode 100644 index 0000000..3341e6e --- /dev/null +++ b/.zsh/00-umask.zsh @@ -0,0 +1,3 @@ +# FILE CREATION MASK +# set default permissions for newly created files and directories +umask 0077 diff --git a/.zsh/10-path.zsh b/.zsh/10-path.zsh new file mode 100644 index 0000000..699a028 --- /dev/null +++ b/.zsh/10-path.zsh @@ -0,0 +1,14 @@ +typeset -U path PATH + +path=( +"$HOME/.local/bin" +"$HOME/.filen-cli/bin" +"$HOME/.cargo/bin" +"$HOME/go/bin" +"$HOME/.npm-global/bin" +/usr/local/bin +/usr/lib +$path +) + +export PATH diff --git a/.zsh/20-options.zsh b/.zsh/20-options.zsh new file mode 100644 index 0000000..0eb5a2b --- /dev/null +++ b/.zsh/20-options.zsh @@ -0,0 +1,23 @@ +# LINTING DIRECTIVES +# ignore file during static analysis due to lack of zsh support +# shellcheck disable=all +setopt AUTO_CD # enter dir without prepending `cd` +setopt AUTO_PUSHD +setopt PUSHD_IGNORE_DUPS +setopt PUSHD_SILENT +setopt CDABLE_VARS +setopt NO_CASE_GLOB +setopt COMPLETE_IN_WORD +setopt ALWAYS_TO_END +setopt AUTO_LIST +setopt GLOB_DOTS +setopt hist_ignore_space + +# GLOBBING OPTIONS +# set expansion rules +setopt EXTENDED_GLOB +setopt NUMERIC_GLOB_SORT + +# SAFETY OPTIONS +# prevent accidental file overwriting +setopt NO_CLOBBER diff --git a/.zsh/30-history.zsh b/.zsh/30-history.zsh new file mode 100644 index 0000000..9dce98d --- /dev/null +++ b/.zsh/30-history.zsh @@ -0,0 +1,22 @@ +# HISTORY FILE CONFIGURATION +# define history location and limits +HISTFILE="$ZDOTDIR/.history" +HISTSIZE=1000 +SAVEHIST=2000 + +# HISTORY OPTIONS +# set recording and retrieval parameters +setopt APPEND_HISTORY +setopt INC_APPEND_HISTORY +setopt HIST_IGNORE_DUPS +setopt HIST_IGNORE_ALL_DUPS +setopt HIST_SAVE_NO_DUPS +setopt HIST_REDUCE_BLANKS +setopt HIST_FIND_NO_DUPS +setopt HIST_VERIFY +setopt SHARE_HISTORY +setopt EXTENDED_HISTORY + +# HISTORY ALIASES +# define output format for history review +alias hist='fc -l -t "%Y-%m-%d %H:%M:%S"' diff --git a/.zsh/40-env.zsh b/.zsh/40-env.zsh new file mode 100644 index 0000000..1ffe93b --- /dev/null +++ b/.zsh/40-env.zsh @@ -0,0 +1,29 @@ +# shellcheck disable=all +# SYSTEM DEFAULTS +# define default editors and pagers +export EDITOR="nvim" +export VISUAL="nvim" +export PAGER="bat" +export MANPAGER="nvim +Man!" +export BAT_THEME="base16" +export NODE_OPTIONS=--no-deprecation + +# HLEDGER CONFIGURATION +# point system commands to the primary journal file +export LEDGER_FILE="$HOME/obsidian/finances/journals/2026.journal" + +# PAGER COLOURS +# define terminal capability codes for less +export LESS_TERMCAP_mb=$'\E[1;31m' +export LESS_TERMCAP_md=$'\E[1;36m' +export LESS_TERMCAP_me=$'\E[0m' +export LESS_TERMCAP_so=$'\E[01;44;33m' +export LESS_TERMCAP_se=$'\E[0m' +export LESS_TERMCAP_us=$'\E[1;32m' +export LESS_TERMCAP_ue=$'\E[0m' + +# VIDEODRIVER +export SDL_VIDEODRIVER=wayland + +export W3M_DIR="$HOME/.config/w3m" +export W3M_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/w3m" diff --git a/.zsh/50-completion.zsh b/.zsh/50-completion.zsh new file mode 100644 index 0000000..f22f7af --- /dev/null +++ b/.zsh/50-completion.zsh @@ -0,0 +1,19 @@ +# COMPLETION INITIALISATION +# load and start the completion system +autoload -Uz compinit +fpath=( + /usr/share/zsh/site-functions + $fpath +) +compinit + +# STYLING OPTIONS +# apply cache and interface rules to completions +zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' +zstyle ':completion:*' menu select +zstyle ':completion:*' list-dirs-first true +zstyle ':completion:*' use-cache yes +zstyle ':completion:*' cache-path ~/.cache/zsh +zstyle ':completion:*' group-name '' +zstyle ':completion:*' smart-case yes +zstyle ':completion:*' completer _expand _complete _correct _approximate diff --git a/.zsh/60-plugins.zsh b/.zsh/60-plugins.zsh new file mode 100644 index 0000000..bf98690 --- /dev/null +++ b/.zsh/60-plugins.zsh @@ -0,0 +1,30 @@ +# STARSHIP INITIALISATION +# start the prompt +eval "$(starship init zsh)" + +# ZOXIDE INITIALISATION +# start the smart directory jumper +if command -v zoxide >/dev/null 2>&1; then + eval "$(zoxide init zsh --cmd cd)" +fi + +# TMUX CHECK +# verify active multiplexer sessions +if [ -n "$TMUX" ] && ! tmux info >/dev/null 2>&1; then + unset TMUX + unset TMUX_PANE +fi + +# ZSH PLUGINS +# source external extension scripts +if [[ -f /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh ]]; then + source /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh +fi + +if [[ -f /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh ]]; then + source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh +fi + +if [[ -f /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]]; then + source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh +fi diff --git a/.zsh/70-binds.zsh b/.zsh/70-binds.zsh new file mode 100644 index 0000000..8fb8bda --- /dev/null +++ b/.zsh/70-binds.zsh @@ -0,0 +1,14 @@ +# INPUT MODES +# enforce vi keybindings +bindkey -e + +# SEARCH BINDINGS +# map arrows to history search functions +bindkey '^[[A' history-substring-search-up +bindkey '^[[B' history-substring-search-down + +# HELP MANUAL +# map keyboard shortcut to local runbook +unalias run-help 2>/dev/null +autoload -Uz run-help +bindkey '^[h' run-help diff --git a/.zsh/80-aliases.zsh b/.zsh/80-aliases.zsh new file mode 100644 index 0000000..29b94f7 --- /dev/null +++ b/.zsh/80-aliases.zsh @@ -0,0 +1,63 @@ +# shellcheck disable=all +alias pm='sudo pacman' +alias pr='paru' + +alias z='cd' + +alias ls='ls --color=always' +alias lz='eza --group-directories-first' +alias l='eza -l --group-directories-first --git' +alias li='eza -laai --group-directories-first --git' +alias la='eza -la --group-directories-first --git' +alias lt='eza -l --tree --group-directories-first --git' +alias lta='eza -la --tree --group-directories-first --git' +alias lt2='eza --tree --level=2' +alias lt3='eza --tree --level=3' +alias cat='bat --paging=never' +alias bap='bat --paging=always' +alias b='bat' + +alias cp='cp -iv' +alias mv='mv -iv' +alias rm='rm -Iv' +alias rmr='rm -Irv' +alias rmf='rm -frv' +alias mkdir='mkdir -pv' + +alias ,='nvim' +alias ,,='sudo nvim' + +alias grep='grep --color=auto' +alias diff='diff --color=auto' +alias ip='ip --color=auto' +alias df='df -h' +alias du='du -h' +alias free='free -h' +alias chmod='chmod -v' +alias suchmod='sudo chmod -v' +alias cls='clear' +alias path='echo $PATH | tr ":" "\n"' +alias reload='exec bash' + +alias -g L='| less' +alias -g G='| grep -i' +alias -g WC='| wc -l' +alias -g NUL='>/dev/null 2>&1' +alias -g E='2>&1' +alias -g C='; clear' +alias -g COPY='| wl-copy' +alias -g ANSICOPY='| ansifilter | wl-copy' + +alias fullcharge='sudo tlp fullcharge' +alias paste='wl-paste' +alias ic='incus' + +alias fastfetch='fastfetch --config examples/25.jsonc' + +alias py='python3' +alias ipy='ipython' + +alias zt='zathura' + +alias sys='systemctl --user' +alias susys='sudo systemctl' diff --git a/.zsh/81-hledger-aliases.zsh b/.zsh/81-hledger-aliases.zsh new file mode 100644 index 0000000..cff0c91 --- /dev/null +++ b/.zsh/81-hledger-aliases.zsh @@ -0,0 +1,10 @@ +alias hl='hledger' +alias hlb='hledger bal' +alias hlbe='hledger bal Expenses' +alias hlbi='hledger bal Income' +alias hlbs='hledger balancesheet' +alias hlr='hledger reg' +alias hlis='hledger incomestatement' +alias hlu='hledger-ui' +alias hlw='hledger-web' +alias hlupdate='md-to-hledger > $HOME/obsidian/finances/journals/compiled.journal && hledger bal' diff --git a/.zsh/82-tmux-aliases.zsh b/.zsh/82-tmux-aliases.zsh new file mode 100644 index 0000000..cf7ede9 --- /dev/null +++ b/.zsh/82-tmux-aliases.zsh @@ -0,0 +1,7 @@ +alias tx='tmux' +alias txn='tmux new -s' +alias txnmain='tmux new -s main' +alias txa='tmux attach -t' +alias txkw='tmux kill-window -t' +alias txks='tmux kill-session -t' +alias txkp='tmux kill-pane -t' diff --git a/.zsh/83-git-aliases.zsh b/.zsh/83-git-aliases.zsh new file mode 100644 index 0000000..7e98c93 --- /dev/null +++ b/.zsh/83-git-aliases.zsh @@ -0,0 +1,21 @@ +alias gs='git status' +alias gst='git status -sb' +alias gl='git log --oneline --graph --decorate --all' +alias gd='git diff' +alias gds='git diff --staged' +alias ga='git add' +alias gaa='git add -A' +alias gc='git commit' +alias gcm='git commit -m' +alias gca='git commit -am' +alias gco='git checkout' +alias gb='git branch' +alias gm='git merge' +alias gp='git push' +alias gpl='git pull' +alias gun='git reset HEAD --' +alias dot='git dot' +alias git.last='git log -1 HEAD' +alias git.oops='git commit --amend --no-edit' +alias git.unstage='git reset HEAD --' +alias git.today="git log --since='24 hours ago' --oneline --graph --all" diff --git a/.zsh/84-network-aliases.zsh b/.zsh/84-network-aliases.zsh new file mode 100644 index 0000000..20ae3bf --- /dev/null +++ b/.zsh/84-network-aliases.zsh @@ -0,0 +1,38 @@ +# IP +alias netlist='ip link show' +alias netip='ip addr show' +alias ports='sudo ss -tulpn' +alias myip='curl -s https://ifconfig.me && echo' +alias iptor='torsocks curl -s https://ifconfig.me' + +# FIREWALL (UFW) +alias fw='sudo ufw' +alias fws='sudo ufw status' +alias fwv='sudo ufw status verbose' +alias fwen='sudo ufw enable' +alias fwdis='sudo ufw disable' +alias fwallow='sudo ufw allow' +alias fwdeny='sudo ufw deny' + +# BLUETOOTH +alias bt='bluetoothctl' +alias bton='bluetoothctl power on' +alias btoff='bluetoothctl power off' +alias btlist='bluetoothctl devices' +alias btscan='bluetoothctl scan on' +alias btconnect='bluetoothctl connect' +alias btdisconnect='bluetoothctl disconnect' + +# VPN & PRIVACY +alias vpnup='sudo wg-quick up proton' +alias vpndown='sudo wg-quick down proton' +alias torup='sudo systemctl start tor && echo "Tor Service Started [O n]"' +alias tordown='sudo systemctl stop tor && echo "Tor Service Stopped [ Off]"' +alias torstat='systemctl status tor' +alias torres='sudo systemctl restart tor' +alias trun='torsocks' +alias tormon='nyx' + +# REFLECTOR +alias mirror='sudo reflector -c FR,DE,NL,GB,PT,ZA,MA --protocol https --latest 20 --sort rate --save /etc/pacman.d/mirrorlist' +#alias mirror='sudo reflector -c FR,DE,NL,GB --protocol https --latest 15 --sort rate --save /etc/pacman.d/mirrorlist' diff --git a/.zsh/85-filen-aliases.zsh b/.zsh/85-filen-aliases.zsh new file mode 100644 index 0000000..696e23c --- /dev/null +++ b/.zsh/85-filen-aliases.zsh @@ -0,0 +1,50 @@ +# shellcheck disable=all +alias fl='filen' +alias flls='filen ls' +alias fllsl='filen ls --long' +alias flcat='filen cat' +alias flhead='filen head' +alias fltail='filen tail' +alias flmk='filen mkdir' +alias flmkdir='filen mkdir' +alias flrm='filen rm' +alias flrmp='filen rm --no-trash' +alias flstat='filen stat' +alias flstatfs='filen statfs' +alias flwho='filen whoami' +alias flmv='filen mv' +alias flcp='filen cp' +alias flcd='filen cd' +alias flup='filen upload' +alias fldown='filen download' +alias flwr='filen write' +alias flwrite='filen write' +alias flopen='filen open' +alias fledit='filen edit' +alias flview='filen view' +alias fldrive='filen drive' +alias flfav='filen favorite' +alias flunfav='filen unfavorite' +alias flfavs='filen favorites' +alias flrec='filen recents' +alias fllinks='filen links' +alias flnotes='filen export-notes' +alias fltr='filen trash' +alias fltrr='filen trash restore' +alias fltrd='filen trash delete' +alias fltre='filen trash empty' +alias flsync='filen sync' +alias flmnt='filen mount' +alias flmount='filen mount' +alias flunmnt='filen unmount' +alias flunmount='filen unmount' +alias flwebdav='filen webdav' +alias fls3='filen s3' +alias fllogin='filen login' +alias fllogout='filen logout' +alias flacc='filen account' +alias flcfg='filen config' +alias flcanary='filen canary' +alias flinst='filen install' +alias flhelp='filen help' +alias flver='filen --version' diff --git a/.zsh/90-functions.zsh b/.zsh/90-functions.zsh new file mode 100644 index 0000000..28ea0b2 --- /dev/null +++ b/.zsh/90-functions.zsh @@ -0,0 +1,2 @@ +fpath=("$ZDOTDIR/.functions" $fpath) +autoload -Uz "$ZDOTDIR"/.functions/*(N.:t) diff --git a/.zsh/README.md b/.zsh/README.md new file mode 100644 index 0000000..5bae29f --- /dev/null +++ b/.zsh/README.md @@ -0,0 +1,23 @@ +# .zsh + +This directory contains a modularised Zsh configuration. The shell environment is broken down into distinct, numbered files to enforce a strict and predictable execution order during the startup sequence. + +## CORE INITIALISATION +* **.zshrc:** The primary interactive shell entry point that sources the subsequent configuration modules. +* **00-umask.zsh:** Defines default file creation permissions. +* **01-path.zsh:** Constructs the `$PATH` variable and defines executable routes for the system. +* **02-options.zsh:** Configures core Zsh behaviour and shell directives via `setopt`. + +## ENVIRONMENT AND STATE +* **03-history.zsh:** Configures command history logging, file size limits, and retention behaviour. +* **04-env.zsh:** Exports global environment variables required by the system and user applications. +* **05-completion.zsh:** Initialises the Zsh completion system (`compinit`) and configures tab-completion behaviour. + +## EXTENSIONS AND WORKFLOW +* **06-plugins.zsh:** Loads shell extensions, syntax highlighting, and prompt integrations. +* **07-binds.zsh:** Defines custom keyboard shortcuts and configures input handling (such as vi-mode bindings). +* **08-aliases.zsh:** Maps custom command shortcuts and overrides for daily utilities. + +## SCRIPTING AND LOGIC +* **09-functions.zsh:** The initialisation script for loading user-defined shell functions. +* **.functions:** A directory containing discrete, standalone shell scripts and complex logical functions sourced into the environment. diff --git a/.zshenv b/.zshenv new file mode 100755 index 0000000..cf92268 --- /dev/null +++ b/.zshenv @@ -0,0 +1 @@ +export ZDOTDIR="$HOME/.zsh" diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..84bd8ff --- /dev/null +++ b/LICENSE @@ -0,0 +1,621 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as a whole. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS