commit b2ec67a71bf75acebfe554354f7179897d47b320 Author: James Brechtel Date: Mon Aug 10 10:45:29 2026 -0400 Initial commit diff --git a/README.org b/README.org new file mode 100644 index 0000000..d470650 --- /dev/null +++ b/README.org @@ -0,0 +1,3 @@ +* James Utils + +This repository contains various shell scripts I use for either daily computer use or home network maintenance. diff --git a/bin/audio b/bin/audio new file mode 100755 index 0000000..c7b31ae --- /dev/null +++ b/bin/audio @@ -0,0 +1,95 @@ +#!/bin/sh +#pacmd set-default-sink alsa_output.usb-Logitech_Logitech_G933_Gaming_Wireless_Headset-00.analog-stereo +#pacmd set-default-source alsa_input.usb-Logitech_Logitech_G933_Gaming_Wireless_Headset-00.analog-mono + +set -e + +QUDELIX_SINK="alsa_output.usb-QTIL_Qudelix-5K_USB_DAC_ABCDEF0123456789-00.analog-stereo" +SPEAKER_SINK="alsa_output.pci-0000_0f_00.4.3.iec958-stereo" +STAGE_SINK="alsa_output.usb-ACTIONS_Stage_V2-00.analog-stereo" +BOSE_SINK="bluez_output.78_2B_64_CE_04_03.1" +FRACTAL_SINK="alsa_output.usb-Fractal_Fractal_Scape_Dongle_00000000911AD5560372-00.analog-stereo" +SNAPCAST_SINK="snapcast_out" +YETI_SOURCE="alsa_input.usb-Blue_Microphones_Yeti_Nano_2231SQ0017G8_888-000476140106-00.analog-stereo" +NOISETORCH_SOURCE="NoiseTorch Microphone for Yeti Nano" +FRACTAL_SOURCE="alsa_input.usb-Fractal_Fractal_Scape_Dongle_00000000911AD5560372-00.mono-fallback" + +switchAudio() { + case "$1" in + "-s") + #echo fractal + echo -e "fractal\0icon\x1faudio-headphones" + #echo snapcast + echo -e "snapcast" + #echo -en "Firefox\0icon\x1ffirefox" | fuzzel --dmenu + echo -e "stage\0icon\x1faudio-speakers" + #echo "stage" + echo -e "bose\0icon\x1fbluetooth" + #echo "qudelix" + echo -e "qudelix\0icon\x1faudio-headphones" + exit 0; + ;; + "qudelix") + local sink="$QUDELIX_SINK" +# local source="$NOISETORCH_SOURCE" + local source="$YETI_SOURCE" + ;; + "speakers") + local sink="$SPEAKER_SINK" + local source="$YETI_SOURCE" + ;; + "stage") + local sink="$STAGE_SINK" + local source="$YETI_SOURCE" + ;; + "bose") + local sink="$BOSE_SINK" + local source="$YETI_SOURCE" + ;; + "fractal") + local sink="$FRACTAL_SINK" + local source="$FRACTAL_SOURCE" + ;; + "snapcast") + local sink="$SNAPCAST_SINK" + local source="$FRACTAL_SOURCE" + ;; + esac; + + local sourceCommand="pactl move-source-output {} $source" + local sinkCommand="pactl move-sink-input {} $sink" + + pactl set-default-sink $sink + pactl list | grep -o -E 'Sink Input #([0-9]+)' | cut -d"#" -f2 | xargs -I '{}' $sinkCommand + #pactl list | grep -o -E 'Source Output #([0-9]+)' | cut -d"#" -f2 | xargs -I '{}' $sourceCommand + pactl set-default-source $source + echo $1 > $HOME/.last_audio + notify-send -a "Audio" "Switched to $1" + +} + +nextAudio() { + local last_audio=$(cat $HOME/.last_audio) + case "$last_audio" in + "fractal") + local next="speakers" + ;; + "speakers") + local next="fractal" + ;; + *) + local next="fractal" + ;; + esac; + + switchAudio $next +} + +case "$1" in + "next") + nextAudio + ;; + *) + switchAudio $1 + ;; +esac; diff --git a/bin/switchrun b/bin/switchrun new file mode 100755 index 0000000..926aabf --- /dev/null +++ b/bin/switchrun @@ -0,0 +1,79 @@ +#!/usr/bin/nu + +let launcher_apps = open $"($env.HOME)/.config/switchrun/apps.nuon" + +def lookup_window_id [running_apps, app_id] { + let matches = $running_apps | where app_id == $app_id + if ($matches | is-empty) { + "none" + } else { + $matches | get id | first | into string + } +} + +def switch_to [target_window_id: string] { + let focus_cmd = {Action: { FocusWindow: { id: ($target_window_id | into int ) } } } + $focus_cmd | to json -r | socat STDIO $env.NIRI_SOCKET +} + +def launch [target_app_id: string] { + let app = ( + $launcher_apps | + where app_id == $target_app_id | + first + ) + print $app.exec + print ...$app.args + if $app != null { run-external $app.exec ...$app.args} else { print "no exec found" } +} + +def main [target_app_id: string] { + let running_apps = niri msg --json windows | from json + let matches = $running_apps | where app_id == $target_app_id + print $matches + if ($matches | is-empty) { + launch $target_app_id + } else { + let focused_matches = $matches | enumerate | where item.is_focused == true | get index + if ($focused_matches | is-empty) { + switch_to (($matches | first | get id) | into string) + } else { + # In the event of multiple matches, switch to the one after the focused one - wrapping around + # That way we will cycle through them + let target_index = ($focused_matches | first) - 1 + print $target_index + print ($matches | length) + let target_window = if ($target_index < 0) { $matches | last } else { ($matches | get $target_index) } + switch_to ($target_window.id | into string) + } + } +} + +def "main list" [tag?: string] { + let running_apps = niri msg --json windows | from json + + # Optionally narrow to apps carrying the requested tag. + let filtered = (if ($tag | is-empty) { + $launcher_apps + } else { + $launcher_apps | where { |app| ($tag in ($app.tags? | default [])) } + }) + + if ($filtered | is-empty) { + print $"No applications match tag '($tag)'" + return + } + + let apps = $filtered | each { |app| $app | insert window_id { lookup_window_id $running_apps $app.app_id } } + # if the selected app_id is running then + # focus it by its window id (first window id for that app id is fine) + # if it's not running then launch it using the app's desktop entry + # + let app_lines = $apps | each { $"($in.name) - [($in.app_id), ($in.window_id)]" } + + let prompt = if ($tag | is-empty) { "Launch/Switch" } else { $"Launch/Switch [($tag)]" } + let selection = $app_lines | each { echo $in } | str join "|" | rofi -dmenu -i -p $prompt -sep "|" + let parsed = $selection | parse "{name} - [{app_id}, {window_id}]" | first + let match = $apps | where app_id == $parsed.app_id | first + if $parsed.window_id == "none" { launch $match.app_id } else { switch_to $match.window_id } +} diff --git a/config/switchrun/apps.nuon b/config/switchrun/apps.nuon new file mode 100644 index 0000000..5ab291f --- /dev/null +++ b/config/switchrun/apps.nuon @@ -0,0 +1,112 @@ +[ { name: "Firefox - Personal" + , app_id: "firefox_personal" + , exec: "/usr/lib/firefox/firefox" + , args: ["--class=firefox", "-P", "personal", "--name=firefox_personal"] + , tags: ["browser"] + } +, { name: "Firefox - Flipstone" + , app_id: "firefox_flipstone" + , exec: "/usr/lib/firefox/firefox" + , args: ["--class=firefox", "-P", "flipstone", "--name=firefox_flipstone"] + , tags: ["browser"] + } +, { name: "Obsidian" + , app_id: "obsidian" + , exec: "/usr/bin/obsidian" + , args: [] + } +, { name: "Slack" + , app_id: "Slack" + , exec: "/usr/bin/slack" + , args: ["--ozone-platform=wayland", "-s", "%U"] + } +, { name: "Emacs" + , app_id: "jmacs" + , exec: "emacsclient" + , args: ["--alternate-editor=", "--create-frame"] + } +, { name: "Flipstone - Standup" + , app_id: "chrome-dokoinnladjmopjidoajilmllagdhbnc-Work" + , exec: "/usr/bin/chromium" + , args: ["--ozone-platform=wayland", "--profile-directory=Work", "--app-id=dokoinnladjmopjidoajilmllagdhbnc"] + , tags: ["browser"] + } +, { name: "Flipstone - Gather" + , app_id: "chrome-monjdkojdghbbohnhnccccmkogahhgpj-Work" + , exec: "/usr/bin/chromium" + , args: ["--ozone-platform=wayland", "--profile-directory=Work", "--app-id=monjdkojdghbbohnhnccccmkogahhgpj"] + , tags: ["browser"] + } +, { name: "Text Messages" + , app_id: "chrome-hpfldicfbfomlpcikngkocigghgafkph-Profile_1" + , exec: "/usr/bin/chromium" + , args: ["--ozone-platform=wayland", "--profile-directory=Profile_1", "--app-id=hpfldicfbfomlpcikngkocigghgafkph"] + } +, { name: "Whatsi (WhatsApp)" + , app_id: "com.ktechpit.whatsie" + , exec: "whatsie" + , args: [] + } +, { name: "Signal" + , app_id: "signal" + , exec: "signal-desktop" + , args: ["--password-store=gnome-libsecret"] + } +, { name: "Chromium" + , app_id: "chromium" + , exec: "chromium" + , args: ["--ozone-platform=wayland"] + , tags: ["browser"] + } +, { name: "Thunar (file manager)" + , app_id: "thunar" + , exec: "thunar" + , args: [] + } +, { name: "Obsidian" + , app_id: "obsidian" + , exec: "/usr/bin/obsidian" + , args: [] + } +, { name: "WezTerm" + , app_id: "org.wezfurlong.wezterm" + , exec: "/usr/bin/wezterm" + , args: [] + , tags: ["terminals"] + } +, { name: "WezTerm-Flipstone-Dev" + , app_id: "lol.roo.flipstone.dev.term" + , exec: "/usr/bin/wezterm" + , args: ["--config", "color_scheme=\"zenbones_dark\"", "start", "--class", "lol.roo.flipstone.dev.term"] + , tags: ["terminals"] + } +, { name: "WezTerm-Flipstone-FSDB" + , app_id: "lol.roo.flipstone.fsdb.term" + , exec: "/usr/bin/wezterm" + , args: ["--config", "color_scheme=\"Urple\"", "start", "--class", "lol.roo.flipstone.fsdb.term"] + , tags: ["terminals"] + } +, { name: "WezTerm-Home" + , app_id: "lol.roo.home.term" + , exec: "/usr/bin/wezterm" + , args: ["--config", "color_scheme=\"Zenburn\"", "start", "--class", "lol.roo.home.term"] + , tags: ["terminals"] + } +, { name: "Heidi" + , app_id: "mpv" + , exec: "mpv" + , args: ["rtsp://camera:PMLVK2Kp4r@192.168.10.63/stream1"] + } +, { name: "Chromium - Personal" + , app_id: "chromium-personal" + , exec: "/usr/bin/chromium" + , args: ["--class=chromium-personal", "--profile-directory=Personal"] + , tags: ["browser"] + } +, { name: "Chromium - Work" + , app_id: "chromium-work" + , exec: "/usr/bin/chromium" + , args: ["--class=chromium-work", "--profile-directory=Work"] + , tags: ["browser"] + } +] diff --git a/install.nu b/install.nu new file mode 100755 index 0000000..cb4fae6 --- /dev/null +++ b/install.nu @@ -0,0 +1,61 @@ +#!/usr/bin/env nu + +# install.nu — install scripts and config from this repo into the user's home. +# +# bin/ -> $HOME/bin (made executable, overwrite silently) +# config/ -> $HOME/.config/ (mirrored recursively, overwrite silently) +# +# The repo root is resolved relative to this script's own location, so it +# works regardless of the current working directory. + +def "repo-root" [] { + $env.CURRENT_FILE | path dirname | path expand +} + +# Recursively mirror a source directory tree into a target directory, +# overwriting existing files and creating needed subdirectories. +# Optionally chmod +x each target file. +def "install-tree" [source_dir: path, target_dir: path, make_exec: bool] { + if not ($source_dir | path exists) { + error make { msg: $"Source directory not found: ($source_dir)" } + } + + mkdir $target_dir + + # Enumerate all files under source_dir (dirs filtered; hidden files included). + let files = (glob -D $"($source_dir)/**/*") + if ($files | is-empty) { + error make { msg: $"No files found in ($source_dir)" } + } + + let installed = ($files | each { |file| + let rel = ($file | path relative-to $source_dir) + let dest = ($target_dir | path join $rel) + # Ensure the parent directory exists, then copy the file. + mkdir ($dest | path dirname) + cp $file $dest + if $make_exec { chmod +x $dest } + $dest + }) + + $installed | each { |path| print $"installed: ($path)" } + + let count = ($installed | length) + print $"installed ($count) entries into ($target_dir)" +} + +def "main" [] { + let root = (repo-root) + + let bin_source = ($root | path join bin) + let bin_target = ($env.HOME | path join bin | path expand) + print $"(ansi cyan_bold)Installing scripts(ansi reset) bin/ -> ($bin_target)" + install-tree $bin_source $bin_target true + + print "" + + let cfg_source = ($root | path join config) + let cfg_target = ($env.HOME | path join .config | path expand) + print $"(ansi cyan_bold)Installing config(ansi reset) config/ -> ($cfg_target)" + install-tree $cfg_source $cfg_target false +}