This commit is contained in:
Solomon Laing 2023-05-25 16:36:19 +09:30
parent dd31193cef
commit de7855905a
20 changed files with 543 additions and 1112 deletions

View File

@ -33,7 +33,7 @@ export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quo
bind '"\C-o":"lfcd\n"' bind '"\C-o":"lfcd\n"'
bind '"\C-t":"tms\n"' bind '"\C-t":"tms\n"'
bind '"\C-T":"tms $(pwd)\n"' bind '"\C-w":"tms $(pwd)\n"'
bind '"\C-n":"zk edit --interactive --sort modified-\n"' bind '"\C-n":"zk edit --interactive --sort modified-\n"'
bind '"\C-f":"cd $(dirname "$(fzf)")\n"' bind '"\C-f":"cd $(dirname "$(fzf)")\n"'

5
.config/mpv/input.conf Normal file
View File

@ -0,0 +1,5 @@
l seek 5
h seek -5
j seek -60
k seek 60
S cycle sub

24
.config/ncmpcpp/bindings Normal file
View File

@ -0,0 +1,24 @@
def_key "j"
scroll_down
def_key "k"
scroll_up
def_key "h"
previous_column
def_key "l"
next_column
def_key "ctrl-b"
page_up
def_key "ctrl-u"
page_up
def_key "ctrl-f"
page_down
def_key "ctrl-d"
page_down
def_key "g"
move_home
def_key "G"
move_end
def_key "n"
next_found_item
def_key "N"
previous_found_item

1
.config/ncmpcpp/config Normal file
View File

@ -0,0 +1 @@
execute_on_song_change = notify-send "Now Playing" "$(mpc --format '%title% \n%artist% - %album%' current)"

View File

@ -1,40 +1,40 @@
local M = {} local M = {}
M.autoformat = true M.autoformat = false
function M.toggle() function M.toggle()
M.autoformat = not M.autoformat M.autoformat = not M.autoformat
vim.notify(M.autoformat and "Enabled format on save" or "Disabled format on save") vim.notify(M.autoformat and "Enabled format on save" or "Disabled format on save")
end end
function M.format() function M.format()
local buf = vim.api.nvim_get_current_buf() local buf = vim.api.nvim_get_current_buf()
local ft = vim.bo[buf].filetype local ft = vim.bo[buf].filetype
local have_nls = #require("null-ls.sources").get_available(ft, "NULL_LS_FORMATTING") > 0 local have_nls = #require("null-ls.sources").get_available(ft, "NULL_LS_FORMATTING") > 0
vim.lsp.buf.format({ vim.lsp.buf.format({
bufnr = buf, bufnr = buf,
filter = function(client) filter = function(client)
if have_nls then if have_nls then
return client.name == "null-ls" return client.name == "null-ls"
end end
return client.name ~= "null-ls" return client.name ~= "null-ls"
end, end,
}) })
end end
function M.on_attach(client, buf) function M.on_attach(client, buf)
if client.supports_method("textDocument/formatting") then if client.supports_method("textDocument/formatting") then
vim.api.nvim_create_autocmd("BufWritePre", { vim.api.nvim_create_autocmd("BufWritePre", {
group = vim.api.nvim_create_augroup("LspFormat." .. buf, {}), group = vim.api.nvim_create_augroup("LspFormat." .. buf, {}),
buffer = buf, buffer = buf,
callback = function() callback = function()
if M.autoformat then if M.autoformat then
M.format() M.format()
end end
end, end,
}) })
end end
end end
return M return M

View File

@ -1,32 +1,32 @@
return { return {
"nvim-neorg/neorg", -- "nvim-neorg/neorg",
-- event = "BufEnter *.norg", -- -- event = "BufEnter *.norg",
event = "VeryLazy", -- event = "VeryLazy",
dependencies = { { "nvim-treesitter/nvim-treesitter" }, { "nvim-lua/plenary.nvim" } }, -- dependencies = { { "nvim-treesitter/nvim-treesitter" }, { "nvim-lua/plenary.nvim" } },
build = ":Neorg sync-parsers", -- build = ":Neorg sync-parsers",
opts = { -- opts = {
load = { -- load = {
["core.defaults"] = {}, -- ["core.defaults"] = {},
["core.concealer"] = {}, -- ["core.concealer"] = {},
["core.dirman"] = { -- ["core.dirman"] = {
config = { -- config = {
workspaces = { -- workspaces = {
work = "~/work/neorg/", -- work = "~/work/neorg/",
personal = "~/notes/neorg/", -- personal = "~/notes/neorg/",
journal = "~/notes/neorg/journal/", -- journal = "~/notes/neorg/journal/",
}, -- },
}, -- },
}, -- },
["core.completion"] = { -- ["core.completion"] = {
config = { -- config = {
engine = "nvim-cmp", -- engine = "nvim-cmp",
}, -- },
}, -- },
["core.presenter"] = { -- ["core.presenter"] = {
config = { -- config = {
zen_mode = "zen-mode", -- zen_mode = "zen-mode",
}, -- },
}, -- },
}, -- },
}, -- },
} }

View File

@ -0,0 +1,247 @@
# Daemon config file for PipeWire version "0.3.40" #
#
# Copy and edit this file in /etc/pipewire for system-wide changes
# or in ~/.config/pipewire for local changes.
context.properties = {
## Configure properties in the system.
#library.name.system = support/libspa-support
#context.data-loop.library.name.system = support/libspa-support
#support.dbus = true
#link.max-buffers = 64
link.max-buffers = 16 # version < 3 clients can't handle more
#mem.warn-mlock = false
#mem.allow-mlock = true
#mem.mlock-all = false
#clock.power-of-two-quantum = true
#log.level = 2
#cpu.zero.denormals = true
core.daemon = true # listening for socket connections
core.name = pipewire-0 # core name and socket name
## Properties for the DSP configuration.
#default.clock.rate = 48000
#default.clock.allowed-rates = [ 48000 ]
#default.clock.quantum = 1024
#default.clock.min-quantum = 32
#default.clock.max-quantum = 8192
#default.video.width = 640
#default.video.height = 480
#default.video.rate.num = 25
#default.video.rate.denom = 1
#
# These overrides are only applied when running in a vm.
vm.overrides = {
default.clock.min-quantum = 1024
}
}
context.spa-libs = {
#<factory-name regex> = <library-name>
#
# Used to find spa factory names. It maps an spa factory name
# regular expression to a library name that should contain
# that factory.
#
audio.convert.* = audioconvert/libspa-audioconvert
api.alsa.* = alsa/libspa-alsa
api.v4l2.* = v4l2/libspa-v4l2
api.libcamera.* = libcamera/libspa-libcamera
api.bluez5.* = bluez5/libspa-bluez5
api.vulkan.* = vulkan/libspa-vulkan
api.jack.* = jack/libspa-jack
support.* = support/libspa-support
#videotestsrc = videotestsrc/libspa-videotestsrc
#audiotestsrc = audiotestsrc/libspa-audiotestsrc
}
context.modules = [
#{ name = <module-name>
# [ args = { <key> = <value> ... } ]
# [ flags = [ [ ifexists ] [ nofail ] ]
#}
#
# Loads a module with the given parameters.
# If ifexists is given, the module is ignored when it is not found.
# If nofail is given, module initialization failures are ignored.
#
# Uses RTKit to boost the data thread priority.
{ name = libpipewire-module-rtkit
args = {
#nice.level = -11
#rt.prio = 88
#rt.time.soft = 2000000
#rt.time.hard = 2000000
}
flags = [ ifexists nofail ]
}
# Set thread priorities without using RTKit.
#{ name = libpipewire-module-rt
# args = {
# nice.level = -11
# rt.prio = 88
# rt.time.soft = 2000000
# rt.time.hard = 2000000
# }
# flags = [ ifexists nofail ]
#}
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# The profile module. Allows application to access profiler
# and performance data. It provides an interface that is used
# by pw-top and pw-profiler.
{ name = libpipewire-module-profiler }
# Allows applications to create metadata objects. It creates
# a factory for Metadata objects.
{ name = libpipewire-module-metadata }
# Creates a factory for making devices that run in the
# context of the PipeWire server.
{ name = libpipewire-module-spa-device-factory }
# Creates a factory for making nodes that run in the
# context of the PipeWire server.
{ name = libpipewire-module-spa-node-factory }
# Allows creating nodes that run in the context of the
# client. Is used by all clients that want to provide
# data to PipeWire.
{ name = libpipewire-module-client-node }
# Allows creating devices that run in the context of the
# client. Is used by the session manager.
{ name = libpipewire-module-client-device }
# The portal module monitors the PID of the portal process
# and tags connections with the same PID as portal
# connections.
{ name = libpipewire-module-portal
flags = [ ifexists nofail ]
}
# The access module can perform access checks and block
# new clients.
{ name = libpipewire-module-access
args = {
# access.allowed to list an array of paths of allowed
# apps.
#access.allowed = [
# /usr/bin/pipewire-media-session
#]
# An array of rejected paths.
#access.rejected = [ ]
# An array of paths with restricted access.
#access.restricted = [ ]
# Anything not in the above lists gets assigned the
# access.force permission.
#access.force = flatpak
}
}
# Makes a factory for wrapping nodes in an adapter with a
# converter and resampler.
{ name = libpipewire-module-adapter }
# Makes a factory for creating links between ports.
{ name = libpipewire-module-link-factory }
# Provides factories to make session manager objects.
{ name = libpipewire-module-session-manager }
]
context.objects = [
#{ factory = <factory-name>
# [ args = { <key> = <value> ... } ]
# [ flags = [ [ nofail ] ]
#}
#
# Creates an object from a PipeWire factory with the given parameters.
# If nofail is given, errors are ignored (and no object is created).
#
#{ factory = spa-node-factory args = { factory.name = videotestsrc node.name = videotestsrc Spa:Pod:Object:Param:Props:patternType = 1 } }
#{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] }
#{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } }
#{ factory = spa-node-factory args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } }
#{ factory = adapter args = { factory.name = audiotestsrc node.name = my-test } }
#{ factory = spa-node-factory args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }
# A default dummy driver. This handles nodes marked with the "node.always-driver"
# property when no other driver is currently active. JACK clients need this.
{ factory = spa-node-factory
args = {
factory.name = support.node.driver
node.name = Dummy-Driver
node.group = pipewire.dummy
priority.driver = 20000
}
}
{ factory = spa-node-factory
args = {
factory.name = support.node.driver
node.name = Freewheel-Driver
priority.driver = 19000
node.group = pipewire.freewheel
node.freewheel = true
}
}
# This creates a new Source node. It will have input ports
# that you can link, to provide audio for this source.
#{ factory = adapter
# args = {
# factory.name = support.null-audio-sink
# node.name = "my-mic"
# node.description = "Microphone"
# media.class = "Audio/Source/Virtual"
# audio.position = "FL,FR"
# }
#}
# This creates a single PCM source device for the given
# alsa device path hw:0. You can change source to sink
# to make a sink in the same way.
#{ factory = adapter
# args = {
# factory.name = api.alsa.pcm.source
# node.name = "alsa-source"
# node.description = "PCM Source"
# media.class = "Audio/Source"
# api.alsa.path = "hw:0"
# api.alsa.period-size = 1024
# api.alsa.headroom = 0
# api.alsa.disable-mmap = false
# api.alsa.disable-batch = false
# audio.format = "S16LE"
# audio.rate = 48000
# audio.channels = 2
# audio.position = "FL,FR"
# }
#}
]
context.exec = [
#{ path = <program-name> [ args = "<arguments>" ] }
#
# Execute the given program with arguments.
#
# You can optionally start the session manager here,
# but it is better to start it as a systemd service.
# Run the session manager with -h for options.
#
{ path = "/usr/bin/wireplumber" args = "" }
#
# You can optionally start the pulseaudio-server here as well
# but it is better to start it as a systemd service.
# It can be interesting to start another daemon here that listens
# on another address with the -a option (eg. -a tcp:4713).
#
{ path = "/usr/bin/pipewire" args = "-c pipewire-pulse.conf" }
]

View File

@ -62,3 +62,13 @@ alias \
cls="clear" \ cls="clear" \
c="clear" \ c="clear" \
cdc="cd && clear" cdc="cd && clear"
# git aliases
alias \
gst="git status" \
ga.="git add ." \
gca="git commit -a" \
gcam="git commit -am" \
gsta="git stash" \
gstac="git stash clear" \
gstaa="git stash apply"

0
.config/wget/wgetrc Normal file
View File

View File

@ -13,8 +13,10 @@ numlockx
# set default desktop window layout # set default desktop window layout
"$HOME/.screenlayout/default.sh" "$HOME/.screenlayout/default.sh"
nitrogen --restore &
# I don't use the following but I like it so I'll leave it for now. # I don't use the following but I like it so I'll leave it for now.
autostart="pipewire pipewire-pulse wireplumber" autostart="pipewire pipewire-pulse wireplumber dunst mpd nm-applet picom"
for program in $autostart; do for program in $autostart; do
pidof -s "$program" || "$program" & pidof -s "$program" || "$program" &

15
.config/zathura/zathurarc Normal file
View File

@ -0,0 +1,15 @@
set sandbox none
set statusbar-h-padding 0
set statusbar-v-padding 0
set page-padding 1
set selection-clipboard clipboard
map u scroll half-up
map d scroll half-down
map D toggle_page_mode
map r reload
map R rotate
map K zoom in
map J zoom out
map i recolor
map p print
map g goto top

View File

@ -7,17 +7,7 @@
# System Applications # System Applications
############################################################################### ###############################################################################
# Dunst - notifications # conflicted about using this and/or xprofile...
dunst &
# Network Manager Applet
nm-applet &
# Compostitor with special stuff
picom &
# wallpaper
nitrogen --restore &
############################################################################### ###############################################################################
# Startup Applications # Startup Applications

View File

@ -1,15 +1,23 @@
#!/bin/bash #!/bin/bash
commands="rsync find man tldr sed awk tr cp ls grep xargs rg ps mv kill lsof less head tail tar cp rm rename jq cat ssh cargo git git-worktree git-status git-commit git-rebase docker docker-compose stow chmod chown make" commands="ncmpcpp rsync find man tldr sed awk tr cp ls grep xargs rg ps mv kill lsof less head tail tar cp rm rename jq cat ssh cargo git git-worktree git-status git-commit git-rebase docker docker-compose stow chmod chown make"
languages="golang solidity vlang v nodejs javascript tmux typescript zsh cpp c lua rust python bash php haskell ArnoldC css html gdb" languages="golang solidity vlang v nodejs javascript tmux typescript zsh cpp c lua rust python bash php haskell ArnoldC css html gdb"
selected=$(echo "$commands $languages" | tr ' ' '\n' | fzf) if [[ -z $@ ]]; then
if [[ -z $selected ]]; then
exit 0 selected=$(echo "$commands $languages" | tr ' ' '\n' | fzf)
if [[ -z $selected ]]; then
exit 0
fi
read -rp "Enter Query: " query
query=$(echo "$query" | tr ' ' '+')
tmux neww bash -c "curl cht.sh/$selected/$query & while [ : ]; do sleep 1; done"
else
tmux neww bash -c "curl cht.sh/$@ & while [ : ]; do sleep 1; done"
fi fi
read -rp "Enter Query: " query
query=$(echo "$query" | tr ' ' '+')
tmux neww bash -c "curl cht.sh/$selected/$query & while [ : ]; do sleep 1; done"

View File

@ -36,31 +36,30 @@ twoscreen() { # If multi-monitor is selected and there are two screens.
direction=$(printf "left\\nright" | dmenu -i -p "What side of $primary should $secondary be on?") direction=$(printf "left\\nright" | dmenu -i -p "What side of $primary should $secondary be on?")
xrandr --output "$primary" --auto --scale 1.0x1.0 --output "$secondary" --"$direction"-of "$primary" --auto --scale 1.0x1.0 xrandr --output "$primary" --auto --scale 1.0x1.0 --output "$secondary" --"$direction"-of "$primary" --auto --scale 1.0x1.0
fi fi
} }
morescreen() { # If multi-monitor is selected and there are more than two screens. morescreen() { # If multi-monitor is selected and there are more than two screens.
primary=$(echo "$screens" | dmenu -i -p "Select primary display:") primary=$(echo "$screens" | dmenu -i -p "Select primary display:")
secondary=$(echo "$screens" | grep -v "$primary" | dmenu -i -p "Select secondary display:") secondary=$(echo "$screens" | grep -v "$primary" | dmenu -i -p "Select secondary display:")
direction=$(printf "left\\nright" | dmenu -i -p "What side of $primary should $secondary be on?") direction=$(printf "left\\nright" | dmenu -i -p "What side of $primary should $secondary be on?")
tertiary=$(echo "$screens" | grep -v "$primary" | grep -v "$secondary" | dmenu -i -p "Select third display:") tertiary=$(echo "$screens" | grep -v "$primary" | grep -v "$secondary" | dmenu -i -p "Select third display:")
xrandr --output "$primary" --auto --output "$secondary" --"$direction"-of "$primary" --auto --output "$tertiary" --"$(printf "left\\nright" | grep -v "$direction")"-of "$primary" --auto xrandr --output "$primary" --auto --output "$secondary" --"$direction"-of "$primary" --auto --output "$tertiary" --"$(printf "left\\nright" | grep -v "$direction")"-of "$primary" --auto
} }
multimon() { # Multi-monitor handler. multimon() { # Multi-monitor handler.
case "$(echo "$screens" | wc -l)" in case "$(echo "$screens" | wc -l)" in
2) twoscreen ;; 2) twoscreen ;;
*) morescreen ;; *) morescreen ;;
esac ;} esac ;}
onescreen() { # If only one output available or chosen. onescreen() { # If only one output available or chosen.
xrandr --output "$1" --auto --scale 1.0x1.0 "$(echo "$allposs" | grep -v "\b$1" | awk '{print "--output", $1, "--off"}' | paste -sd ' ' -)" xrandr --output "$1" --auto --scale 1.0x1.0 $(echo "$allposs" | grep -v "\b$1" | awk '{print "--output", $1, "--off"}' | paste -sd ' ' -)
} }
postrun() { # Stuff to run to clean up. postrun() { # Stuff to run to clean up.
setbg # Fix background if screen size/arangement has changed. setbg # Fix background if screen size/arangement has changed.
remaps # Re-remap keys if keyboard added (for laptop bases) { killall dunst ; setsid -f dunst ;} >/dev/null 2>&1 # Restart dunst to ensure proper location on screen
{ killall dunst ; setsid -f dunst ;} >/dev/null 2>&1 # Restart dunst to ensure proper location on screen }
}
# Get all possible displays # Get all possible displays
allposs=$(xrandr -q | grep "connected") allposs=$(xrandr -q | grep "connected")
@ -70,14 +69,14 @@ screens=$(echo "$allposs" | awk '/ connected/ {print $1}')
# If there's only one screen # If there's only one screen
[ "$(echo "$screens" | wc -l)" -lt 2 ] && [ "$(echo "$screens" | wc -l)" -lt 2 ] &&
{ onescreen "$screens"; postrun; notify-send "💻 Only one screen detected." "Using it in its optimal settings..."; exit ;} { onescreen "$screens"; postrun; notify-send "💻 Only one screen detected." "Using it in its optimal settings..."; exit ;}
# Get user choice including multi-monitor and manual selection: # Get user choice including multi-monitor and manual selection:
chosen=$(printf "%s\\nmulti-monitor\\nmanual selection" "$screens" | dmenu -i -p "Select display arangement:") && chosen=$(printf "%s\\nmulti-monitor\\nmanual selection" "$screens" | dmenu -i -p "Select display arangement:") &&
case "$chosen" in case "$chosen" in
"manual selection") arandr ; exit ;; "manual selection") arandr ; exit ;;
"multi-monitor") multimon ;; "multi-monitor") multimon ;;
*) onescreen "$chosen" ;; *) onescreen "$chosen" ;;
esac esac
postrun postrun

View File

@ -1,67 +0,0 @@
#!/bin/sh
# Gives a dmenu prompt to mount unmounted drives and Android phones. If
# they're in /etc/fstab, they'll be mounted automatically. Otherwise, you'll
# be prompted to give a mountpoint from already existsing directories. If you
# input a novel directory, it will prompt you to create that directory.
getmount() { \
[ -z "$chosen" ] && exit 1
# shellcheck disable=SC2086
mp="$(find $1 2>/dev/null | dmenu -i -p "Type in mount point.")" || exit 1
test -z "$mp" && exit 1
if [ ! -d "$mp" ]; then
mkdiryn=$(printf "No\\nYes" | dmenu -i -p "$mp does not exist. Create it?") || exit 1
[ "$mkdiryn" = "Yes" ] && (mkdir -p "$mp" || sudo -A mkdir -p "$mp")
fi
}
mountusb() { \
chosen="$(echo "$usbdrives" | dmenu -i -p "Mount which drive?")" || exit 1
chosen="$(echo "$chosen" | awk '{print $1}')"
sudo -A mount "$chosen" 2>/dev/null && notify-send "USB mounting" "$chosen mounted." && exit 0
alreadymounted=$(lsblk -nrpo "name,type,mountpoint" | awk '$3!~/\/boot|\/home$|SWAP/&&length($3)>1{printf "-not ( -path *%s -prune ) ",$3}')
getmount "/mnt /media /mount /home -maxdepth 5 -type d $alreadymounted"
partitiontype="$(lsblk -no "fstype" "$chosen")"
case "$partitiontype" in
"vfat") sudo -A mount -t vfat "$chosen" "$mp" -o rw,umask=0000 ;;
"exfat") sudo -A mount "$chosen" "$mp" -o uid="$(id -u)",gid="$(id -g)" ;;
*) sudo -A mount "$chosen" "$mp"; user="$(whoami)"; ug="$(groups | awk '{print $1}')"; sudo -A chown "$user":"$ug" "$mp" ;;
esac && notify-send "USB mounting" "$chosen mounted to $mp." ||
notify-send "Drive failed to mount." "Probably a permissions issue or drive is already mounted."
}
mountandroid() { \
chosen="$(echo "$anddrives" | dmenu -i -p "Which Android device?")" || exit 1
chosen="$(echo "$chosen" | cut -d : -f 1)"
getmount "$HOME -maxdepth 3 -type d"
echo "OK" | dmenu -i -p "Tap Allow on your phone if it asks for permission and then press enter" || exit 1
simple-mtpfs --device "$chosen" "$mp" &&
notify-send "Android Mounting" "Android device mounted to $mp." ||
notify-send "Android failed mounting." "Probably a permissions issue or phone is already mounted."
}
asktype() { \
choice="$(printf "USB\\nAndroid" | dmenu -i -p "Mount a USB drive or Android device?")" || exit 1
case $choice in
USB) mountusb ;;
Android) mountandroid ;;
esac
}
anddrives=$(simple-mtpfs -l 2>/dev/null)
usbdrives="$(lsblk -rpo "name,type,size,label,mountpoint,fstype" | grep -v crypto_LUKS | grep 'part\|rom' | sed 's/ /:/g' | awk -F':' '$5==""{printf "%s (%s) %s\n",$1,$3,$4}')"
if [ -z "$usbdrives" ]; then
[ -z "$anddrives" ] && echo "No USB drive or Android device detected" && exit
echo "Android device(s) detected."
mountandroid
else
if [ -z "$anddrives" ]; then
echo "USB drive(s) detected."
mountusb
else
echo "Mountable USB drive(s) and Android device(s) detected."
asktype
fi
fi

View File

@ -1,21 +0,0 @@
#!/bin/sh
# A dmenu prompt to unmount drives.
# Provides you with mounted partitions, select one to unmount.
# Drives mounted at /, /boot and /home will not be options to unmount.
drives="$(lsblk -nrpo "name,type,size,mountpoint,label" | awk -F':' '{gsub(/ /,":")}$4!~/\/boot|\/efi|\/home$|SWAP/&&length($4)>1{printf "%s (%s) %s\n",$4,$3,$5}'; awk '/simple-mtpfs/ { print "📱", $2; }' /etc/mtab)"
chosen="$(echo "$drives" | dmenu -i -p "Unmount which drive?")" || exit 1
case "$chosen" in
📱*)
chosen="${chosen#📱 }"
sudo -A umount -l "$chosen"
;;
*)
chosen="${chosen% (*}"
sudo -A umount -l "$chosen"
;;
esac && notify-send "Drive unmounted." "$chosen successfully unmounted." ||
notify-send "Drive failed to unmount." "Possibly a permissions or I/O issue."

114
.local/bin/mounter Executable file
View File

@ -0,0 +1,114 @@
#!/bin/bash
# Mounts Android Phones and USB drives (encrypted or not). This script will
# replace the older `dmenumount` which had extra steps and couldn't handle
# encrypted drives.
# TODO: Try decrypt for drives in crtypttab
# TODO: Add some support for connecting iPhones (although they are annoying).
IFS='
'
# Function for escaping cell-phone names.
escape(){ echo "$@" | iconv -cf UTF-8 -t ASCII//TRANSLIT | tr -d '[:punct:]' | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | sed "s/-\+/-/g;s/\(^-\|-\$\)//g" ;}
# Check for phones.
phones="$(simple-mtpfs -l 2>/dev/null | sed "s/^/📱/")"
mountedphones="$(grep "simple-mtpfs" /etc/mtab)"
# If there are already mounted phones, remove them from the list of mountables.
[ -n "$mountedphones" ] && phones="$(for phone in $phones; do
for mounted in $mountedphones; do
escphone="$(escape "$phone")"
[[ "$mounted" =~ "$escphone" ]] && break 1
done && continue 1
echo "$phone"
done)"
# Check for drives.
lsblkoutput="$(lsblk -rpo "uuid,name,type,size,label,mountpoint,fstype")"
# Get all LUKS drives
allluks="$(echo "$lsblkoutput" | grep crypto_LUKS)"
# Get a list of the LUKS drive UUIDs already decrypted.
decrypted="$(find /dev/disk/by-id/dm-uuid-CRYPT-LUKS2-* | sed "s|.*LUKS2-||;s|-.*||")"
# Functioning for formatting drives correctly for dmenu:
filter() { sed "s/ /:/g" | awk -F':' '$7==""{printf "%s%s (%s) %s\n",$1,$3,$5,$6}' ; }
# Get only LUKS drives that are not decrypted.
unopenedluks="$(for drive in $allluks; do
uuid="${drive%% *}"
uuid="${uuid//-}" # This is a bashism.
for open in $decrypted; do
[ "$uuid" = "$open" ] && break 1
done && continue 1
echo "🔒 $drive"
done | filter)"
# Get all normal, non-encrypted or decrypted partitions that are not mounted.
normalparts="$(echo "$lsblkoutput"| grep -v crypto_LUKS | grep 'part\|rom\|crypt' | sed "s/^/💾 /" | filter )"
# Add all to one variable. If no mountable drives found, exit.
alldrives="$(echo "$phones
$unopenedluks
$normalparts" | sed "/^$/d;s/ *$//")"
# Quit the script if a sequential command fails.
set -e
test -n "$alldrives"
# Feed all found drives to dmenu and get user choice.
chosen="$(echo "$alldrives" | dmenu -p "Mount which drive?" -i)"
# Function for prompting user for a mountpoint.
getmount(){
mp="$(find /mnt /media /mount /home -maxdepth 1 -type d 2>/dev/null | dmenu -i -p "Mount this drive where?")"
test -n "$mp"
if [ ! -d "$mp" ]; then
mkdiryn=$(printf "No\\nYes" | dmenu -i -p "$mp does not exist. Create it?")
[ "$mkdiryn" = "Yes" ] && (mkdir -p "$mp" || sudo -A mkdir -p "$mp")
fi
}
attemptmount(){
# Attempt to mount without a mountpoint, to see if drive is in fstab.
sudo -A mount "$chosen" || return 1
notify-send "💾Drive Mounted." "$chosen mounted."
exit
}
case "$chosen" in
💾*)
chosen="${chosen%% *}"
chosen="${chosen:1}" # This is a bashism.
attemptmount || getmount
sudo -A mount "$chosen" "$mp" -o uid="$(id -u)",gid="$(id -g)"
notify-send "💾Drive Mounted." "$chosen mounted to $mp."
;;
🔒*)
chosen="${chosen%% *}"
chosen="${chosen:1}" # This is a bashism.
# Number the drive.
while true; do
[ -f "/dev/mapper/usb$num" ] || break
num="$(printf "%02d" "$((num +1))")"
done
# Decrypt in a terminal window
${TERMINAL:-st} -n floatterm -g 60x1 -e sudo cryptsetup open "$chosen" "usb$num"
# Check if now decrypted.
test -b "/dev/mapper/usb$num"
attemptmount || getmount
sudo -A mount "/dev/mapper/usb$num" "$mp" -o uid="$(id -u)",gid="$(id -g)"
notify-send "🔓Decrypted drive Mounted." "$chosen decrypted and mounted to $mp."
;;
📱*)
notify-send "❗Note" "Remember to allow file access on your phone now."
getmount
number="${chosen%%:*}"
number="${chosen:1}" # This is a bashism.
sudo -A simple-mtpfs -o allow_other -o fsname="simple-mtpfs-$(escape "$chosen")" --device "$number" "$mp"
notify-send "🤖 Android Mounted." "Android device mounted to $mp."
;;
esac

View File

@ -1,924 +0,0 @@
#!/usr/bin/env python3
# encoding:utf8
"""NetworkManager command line dmenu script.
To add new connections or enable/disable networking requires policykit
permissions setup per:
https://wiki.archlinux.org/index.php/NetworkManager#Set_up_PolicyKit_permissions
OR running the script as root
Add dmenu options and default terminal if desired to
~/.config/networkmanager-dmenu/config.ini
"""
import pathlib
import struct
import configparser
import locale
import os
from os.path import expanduser
import shlex
from shutil import which
import sys
from time import sleep
import uuid
import subprocess
# pylint: disable=import-error
import gi
gi.require_version('NM', '1.0')
from gi.repository import GLib, NM # noqa pylint: disable=wrong-import-position
# pylint: enable=import-error
ENV = os.environ.copy()
ENV['LC_ALL'] = 'C'
ENC = locale.getpreferredencoding()
CONF = configparser.ConfigParser()
CONF.read(expanduser("~/.config/networkmanager-dmenu/config.ini"))
def cli_args():
""" Don't override dmenu_cmd function arguments with CLI args. Removes -l
and -p if those are passed on the command line.
Exception: if -l is passed and dmenu_command is not defined, assume that the
user wants to switch dmenu to the vertical layout and include -l.
Returns: List of additional CLI arguments
"""
args = sys.argv[1:]
cmd = CONF.get('dmenu', 'dmenu_command', fallback=False)
if "-l" in args or "-p" in args:
for nope in ['-l', '-p'] if cmd is not False else ['-p']:
try:
nope_idx = args.index(nope)
del args[nope_idx]
del args[nope_idx]
except ValueError:
pass
return args
def dmenu_pass(command, color):
"""Check if dmenu passphrase patch is applied and return the correct command
line arg list
Args: command - string
color - obscure color string
Returns: list or None
"""
if command != 'dmenu':
return None
try:
# Check for dmenu password patch
dm_patch = b'P' in subprocess.run(["dmenu", "-h"],
capture_output=True,
check=False).stderr
except FileNotFoundError:
dm_patch = False
return ["-P"] if dm_patch else ["-nb", color, "-nf", color]
def dmenu_cmd(num_lines, prompt="Networks", active_lines=None):
"""Parse config.ini for menu options
Args: args - num_lines: number of lines to display
prompt: prompt to show
active_lines: list of line numbers to tag as active
Returns: command invocation (as a list of strings) for example
["dmenu", "-l", "<num_lines>", "-p", "<prompt>", "-i"]
"""
# Create command string
commands = {"dmenu": ["-p", str(prompt)],
"rofi": ["-dmenu", "-p", str(prompt), "-l", str(num_lines)],
"bemenu": ["-p", str(prompt)],
"wofi": ["-p", str(prompt)]}
command = shlex.split(CONF.get('dmenu', 'dmenu_command', fallback="dmenu"))
command.extend(cli_args())
command.extend(commands.get(command[0], []))
# Rofi Highlighting
rofi_highlight = CONF.getboolean('dmenu', 'rofi_highlight', fallback=False)
if rofi_highlight is True and command[0] == "rofi" and active_lines:
command.extend(["-a", ",".join([str(num) for num in active_lines])])
# Passphrase prompts
obscure = CONF.getboolean('dmenu_passphrase', 'obscure', fallback=False)
if prompt == "Passphrase" and obscure is True:
obscure_color = CONF.get('dmenu_passphrase', 'obscure_color', fallback='#222222')
pass_prompts = {"dmenu": dmenu_pass(command[0], obscure_color),
"rofi": ['-password'],
"bemenu": ['-x'],
"wofi": ['-P']}
command.extend(pass_prompts.get(command[0], []))
return command
def choose_adapter(client):
"""If there is more than one wifi adapter installed, ask which one to use
"""
devices = client.get_devices()
devices = [i for i in devices if i.get_device_type() == NM.DeviceType.WIFI]
if not devices:
return None
if len(devices) == 1:
return devices[0]
device_names = "\n".join([d.get_iface() for d in devices])
sel = subprocess.run(dmenu_cmd(len(devices), "CHOOSE ADAPTER:"),
capture_output=True,
check=False,
env=ENV,
input=device_names,
encoding=ENC).stdout
if not sel.strip():
sys.exit()
devices = [i for i in devices if i.get_iface() == sel.strip()]
assert len(devices) == 1
return devices[0]
def is_installed(cmd):
"""Check if a utility is installed"""
return which(cmd) is not None
def bluetooth_get_enabled():
"""Check if bluetooth is enabled via rfkill.
Returns None if no bluetooth device was found.
"""
# See https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-class-rfkill
for path in pathlib.Path('/sys/class/rfkill/').glob('rfkill*'):
if (path / 'type').read_text().strip() == 'bluetooth':
return (path / 'soft').read_text().strip() == '0'
return None
def create_other_actions(client):
"""Return list of other actions that can be taken
"""
networking_enabled = client.networking_get_enabled()
networking_action = "Disable" if networking_enabled else "Enable"
wifi_enabled = client.wireless_get_enabled()
wifi_action = "Disable" if wifi_enabled else "Enable"
bluetooth_enabled = bluetooth_get_enabled()
bluetooth_action = "Disable" if bluetooth_enabled else "Enable"
actions = [Action(f"{wifi_action} Wifi", toggle_wifi,
not wifi_enabled),
Action(f"{networking_action} Networking",
toggle_networking, not networking_enabled)]
if bluetooth_enabled is not None:
actions.append(Action(f"{bluetooth_action} Bluetooth",
toggle_bluetooth, not bluetooth_enabled))
actions += [Action("Launch Connection Manager", launch_connection_editor),
Action("Delete a Connection", delete_connection)]
if wifi_enabled:
actions.append(Action("Rescan Wifi Networks", rescan_wifi))
return actions
def rescan_wifi():
"""
Rescan Wifi Access Points
"""
delay = CONF.getint('nmdm', 'rescan_delay', fallback=5)
for dev in CLIENT.get_devices():
if gi.repository.NM.DeviceWifi == type(dev):
try:
dev.request_scan_async(None, rescan_cb, None)
LOOP.run()
sleep(delay)
notify("Wifi scan complete")
main()
except gi.repository.GLib.Error as err:
# Too frequent rescan error
notify("Wifi rescan failed", urgency="critical")
if not err.code == 6: # pylint: disable=no-member
raise err
def rescan_cb(dev, res, data):
"""Callback for rescan_wifi. Just for notifications
"""
if dev.request_scan_finish(res) is True:
notify("Wifi scan running...")
else:
notify("Wifi scan failed", urgency="critical")
LOOP.quit()
def ssid_to_utf8(nm_ap):
""" Convert binary ssid to utf-8 """
ssid = nm_ap.get_ssid()
if not ssid:
return ""
ret = NM.utils_ssid_to_utf8(ssid.get_data())
return ret
def prompt_saved(saved_cons):
"""Prompt for a saved connection."""
actions = create_saved_actions(saved_cons)
sel = get_selection(actions)
sel()
def ap_security(nm_ap):
"""Parse the security flags to return a string with 'WPA2', etc. """
flags = nm_ap.get_flags()
wpa_flags = nm_ap.get_wpa_flags()
rsn_flags = nm_ap.get_rsn_flags()
sec_str = ""
if ((flags & getattr(NM, '80211ApFlags').PRIVACY) and
(wpa_flags == 0) and (rsn_flags == 0)):
sec_str = " WEP"
if wpa_flags:
sec_str = " WPA1"
if rsn_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_PSK:
sec_str += " WPA2"
if rsn_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_SAE:
sec_str += " WPA3"
if ((wpa_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_802_1X) or
(rsn_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_802_1X)):
sec_str += " 802.1X"
if ((wpa_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_OWE) or
(rsn_flags & getattr(NM, '80211ApSecurityFlags').KEY_MGMT_OWE)):
sec_str += " OWE"
# If there is no security use "--"
if sec_str == "":
sec_str = "--"
return sec_str.lstrip()
class Action(): # pylint: disable=too-few-public-methods
"""Helper class to execute functions from a string variable"""
def __init__(self,
name,
func,
args=None,
active=False):
self.name = name
self.func = func
self.is_active = active
if args is None:
self.args = None
elif isinstance(args, list):
self.args = args
else:
self.args = [args]
def __str__(self):
return self.name
def __call__(self):
if self.args is None:
self.func()
else:
self.func(*self.args)
def conn_matches_adapter(conn, adapter):
"""Return True if the connection is applicable for the given adapter.
There seem to be two ways for a connection specify what interface it belongs
to:
- By setting 'mac-address' in [wifi] to the adapter's MAC
- By setting 'interface-name` in [connection] to the adapter's name.
Depending on how the connection was added, it seems like either
'mac-address', 'interface-name' or neither of both is set.
"""
# [wifi] mac-address
setting_wireless = conn.get_setting_wireless()
mac = setting_wireless.get_mac_address()
if mac is not None:
return mac == adapter.get_permanent_hw_address()
# [connection] interface-name
setting_connection = conn.get_setting_connection()
interface = setting_connection.get_interface_name()
if interface is not None:
return interface == adapter.get_iface()
# Neither is set, let's assume this connection is for multiple/all adapters.
return True
def process_ap(nm_ap, is_active, adapter):
"""Activate/Deactivate a connection and get password if required"""
if is_active:
CLIENT.deactivate_connection_async(nm_ap, None, deactivate_cb, nm_ap)
LOOP.run()
else:
conns_cur = [i for i in CONNS if
i.get_setting_wireless() is not None and
conn_matches_adapter(i, adapter)]
con = nm_ap.filter_connections(conns_cur)
if len(con) > 1:
raise ValueError("There are multiple connections possible")
if len(con) == 1:
CLIENT.activate_connection_async(con[0], adapter, nm_ap.get_path(),
None, activate_cb, nm_ap)
LOOP.run()
else:
if ap_security(nm_ap) != "--":
password = get_passphrase()
else:
password = ""
set_new_connection(nm_ap, password, adapter)
def activate_cb(dev, res, data):
"""Notification if activate connection completed successfully
"""
try:
conn = dev.activate_connection_finish(res)
except GLib.Error:
conn = None
if conn is not None:
notify(f"Activated {conn.get_id()}")
else:
notify(f"Problem activating {data.get_id()}", urgency="critical")
LOOP.quit()
def deactivate_cb(dev, res, data):
"""Notification if deactivate connection completed successfully
"""
if dev.deactivate_connection_finish(res) is True:
notify(f"Deactivated {data.get_id()}")
else:
notify(f"Problem deactivating {data.get_id()}", urgency="critical")
LOOP.quit()
def process_vpngsm(con, activate):
"""Activate/deactive VPN or GSM connections"""
if activate:
CLIENT.activate_connection_async(con, None, None,
None, activate_cb, con)
else:
CLIENT.deactivate_connection_async(con, None, deactivate_cb, con)
LOOP.run()
def create_ap_actions(aps, active_ap, active_connection, adapter): # noqa pylint: disable=too-many-locals,line-too-long
"""For each AP in a list, create the string and its attached function
(activate/deactivate)
"""
active_ap_bssid = active_ap.get_bssid() if active_ap is not None else ""
names = [ssid_to_utf8(ap) for ap in aps]
max_len_name = max([len(name) for name in names]) if names else 0
secs = [ap_security(ap) for ap in aps]
max_len_sec = max([len(sec) for sec in secs]) if secs else 0
ap_actions = []
for nm_ap, name, sec in zip(aps, names, secs):
bars = NM.utils_wifi_strength_bars(nm_ap.get_strength())
wifi_chars = CONF.get("dmenu", "wifi_chars", fallback=False)
if wifi_chars:
bars = "".join([wifi_chars[i] for i, j in enumerate(bars) if j == '*'])
is_active = nm_ap.get_bssid() == active_ap_bssid
compact = CONF.getboolean("dmenu", "compact", fallback=False)
if compact:
action_name = f"{name} {sec} {bars}"
else:
action_name = f"{name:<{max_len_name}s} {sec:<{max_len_sec}s} {bars:>4}"
if is_active:
ap_actions.append(Action(action_name, process_ap,
[active_connection, True, adapter],
active=True))
else:
ap_actions.append(Action(action_name, process_ap,
[nm_ap, False, adapter]))
return ap_actions
def create_vpn_actions(vpns, active):
"""Create the list of strings to display with associated function
(activate/deactivate) for VPN connections.
"""
active_vpns = [i for i in active if i.get_vpn()]
return _create_vpngsm_actions(vpns, active_vpns, "VPN")
def create_wireguard_actions(wgs, active):
"""Create the list of strings to display with associated function
(activate/deactivate) for Wireguard connections.
"""
active_wgs = [i for i in active if i.get_connection_type() == "wireguard"]
return _create_vpngsm_actions(wgs, active_wgs, "Wireguard")
def create_eth_actions(eths, active):
"""Create the list of strings to display with associated function
(activate/deactivate) for Ethernet connections.
"""
active_eths = [i for i in active if 'ethernet' in i.get_connection_type()]
return _create_vpngsm_actions(eths, active_eths, "Eth")
def create_gsm_actions(gsms, active):
"""Create the list of strings to display with associated function
(activate/deactivate) GSM connections."""
active_gsms = [i for i in active if
i.get_connection() is not None and
i.get_connection().is_type(NM.SETTING_GSM_SETTING_NAME)]
return _create_vpngsm_actions(gsms, active_gsms, "GSM")
def create_blue_actions(blues, active):
"""Create the list of strings to display with associated function
(activate/deactivate) Bluetooth connections."""
active_blues = [i for i in active if
i.get_connection() is not None and
i.get_connection().is_type(NM.SETTING_BLUETOOTH_SETTING_NAME)]
return _create_vpngsm_actions(blues, active_blues, "Bluetooth")
def create_saved_actions(saved):
"""Create the list of strings to display with associated function
(activate/deactivate) for VPN connections.
"""
return _create_vpngsm_actions(saved, [], "SAVED")
def _create_vpngsm_actions(cons, active_cons, label):
active_con_ids = [a.get_id() for a in active_cons]
actions = []
for con in cons:
is_active = con.get_id() in active_con_ids
action_name = f"{con.get_id()}:{label}"
if is_active:
active_connection = [a for a in active_cons
if a.get_id() == con.get_id()]
if len(active_connection) != 1:
raise ValueError(f"Multiple active connections match {con.get_id()}")
active_connection = active_connection[0]
actions.append(Action(action_name, process_vpngsm,
[active_connection, False], active=True))
else:
actions.append(Action(action_name, process_vpngsm,
[con, True]))
return actions
def create_wwan_actions(client):
"""Create WWWAN actions
"""
wwan_enabled = client.wwan_get_enabled()
wwan_action = "Disable" if wwan_enabled else "Enable"
return [Action(f"{wwan_action} WWAN", toggle_wwan, not wwan_enabled)]
def combine_actions(eths, aps, vpns, wgs, gsms, blues, wwan, others, saved):
# pylint: disable=too-many-arguments
"""Combine all given actions into a list of actions.
Args: args - eths: list of Actions
aps: list of Actions
vpns: list of Actions
gsms: list of Actions
blues: list of Actions
wwan: list of Actions
others: list of Actions
"""
compact = CONF.getboolean("dmenu", "compact", fallback=False)
empty_action = [Action('', None)] if not compact else []
all_actions = []
all_actions += eths + empty_action if eths else []
all_actions += aps + empty_action if aps else []
all_actions += vpns + empty_action if vpns else []
all_actions += wgs + empty_action if wgs else []
all_actions += gsms + empty_action if (gsms and wwan) else []
all_actions += blues + empty_action if blues else []
all_actions += wwan + empty_action if wwan else []
all_actions += others + empty_action if others else []
all_actions += saved + empty_action if saved else []
return all_actions
def get_selection(all_actions):
"""Spawn dmenu for selection and execute the associated action."""
rofi_highlight = CONF.getboolean('dmenu', 'rofi_highlight', fallback=False)
inp = []
if rofi_highlight is True:
inp = [str(action) for action in all_actions]
else:
inp = [('== ' if action.is_active else ' ') + str(action)
for action in all_actions]
active_lines = [index for index, action in enumerate(all_actions)
if action.is_active]
command = dmenu_cmd(len(inp), active_lines=active_lines)
sel = subprocess.run(command,
capture_output=True,
check=False,
input="\n".join(inp),
encoding=ENC,
env=ENV).stdout
if not sel.rstrip():
sys.exit()
if rofi_highlight is False:
action = [i for i in all_actions
if ((str(i).strip() == str(sel.strip())
and not i.is_active) or
('== ' + str(i) == str(sel.rstrip('\n'))
and i.is_active))]
else:
action = [i for i in all_actions if str(i).strip() == sel.strip()]
assert len(action) == 1, f"Selection was ambiguous: '{str(sel.strip())}'"
return action[0]
def toggle_networking(enable):
"""Enable/disable networking
Args: enable - boolean
"""
toggle = GLib.Variant.new_tuple(GLib.Variant.new_boolean(enable))
try:
CLIENT.dbus_call(NM.DBUS_PATH, NM.DBUS_INTERFACE, "Enable", toggle,
None, -1, None, None, None)
except AttributeError:
# Workaround for older versions of python-gobject
CLIENT.networking_set_enabled(enable)
notify(f"Networking {'enabled' if enable is True else 'disabled'}")
def toggle_wifi(enable):
"""Enable/disable Wifi
Args: enable - boolean
"""
toggle = GLib.Variant.new_boolean(enable)
try:
CLIENT.dbus_set_property(NM.DBUS_PATH, NM.DBUS_INTERFACE, "WirelessEnabled", toggle,
-1, None, None, None)
except AttributeError:
# Workaround for older versions of python-gobject
CLIENT.wireless_set_enabled(enable)
notify(f"Wifi {'enabled' if enable is True else 'disabled'}")
def toggle_wwan(enable):
"""Enable/disable WWAN
Args: enable - boolean
"""
toggle = GLib.Variant.new_boolean(enable)
try:
CLIENT.dbus_set_property(NM.DBUS_PATH, NM.DBUS_INTERFACE, "WwanEnabled", toggle,
-1, None, None, None)
except AttributeError:
# Workaround for older versions of python-gobject
CLIENT.wwan_set_enabled(enable)
notify(f"Wwan {'enabled' if enable is True else 'disabled'}")
def toggle_bluetooth(enable):
"""Enable/disable Bluetooth
Args: enable - boolean
References:
https://github.com/blueman-project/blueman/blob/master/blueman/plugins/mechanism/RfKill.py
https://www.kernel.org/doc/html/latest/driver-api/rfkill.html
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/uapi/linux/rfkill.h?h=v5.8.9
"""
type_bluetooth = 2
op_change_all = 3
idx = 0
soft_state = 0 if enable else 1
hard_state = 0
data = struct.pack("IBBBB", idx, type_bluetooth, op_change_all,
soft_state, hard_state)
try:
with open('/dev/rfkill', 'r+b', buffering=0) as rff:
rff.write(data)
except PermissionError:
notify("Lacking permission to write to /dev/rfkill.",
"Check README for configuration options.",
urgency="critical")
else:
notify(f"Bluetooth {'enabled' if enable else 'disabled'}")
def launch_connection_editor():
"""Launch nmtui or the gui nm-connection-editor
"""
terminal = CONF.get("editor", "terminal", fallback="xterm")
gui_if_available = CONF.getboolean("editor", "gui_if_available", fallback=True)
guis = ["gnome-control-center", "nm-connection-editor"]
if gui_if_available is True:
for gui in guis:
if is_installed(gui):
subprocess.run(gui, check=False)
return
if is_installed("nmtui"):
subprocess.run([terminal, "-e", "nmtui"], check=False)
return
notify("No network connection editor installed", urgency="critical")
def get_passphrase():
"""Get a password
Returns: string
"""
pinentry = CONF.get("dmenu", "pinentry", fallback=None)
if pinentry:
pin = ""
out = subprocess.run(pinentry,
capture_output=True,
check=False,
encoding=ENC,
input='setdesc Get network password\ngetpin\n').stdout
if out:
res = out.split("\n")[2]
if res.startswith("D "):
pin = res.split("D ")[1]
return pin
return subprocess.run(dmenu_cmd(0, "Passphrase"),
stdin=subprocess.DEVNULL,
capture_output=True,
check=False,
encoding=ENC).stdout
def delete_connection():
"""Display list of NM connections and delete the selected one
"""
conn_acts = [Action(i.get_id(), i.delete_async, args=[None, delete_cb, None]) for i in CONNS]
conn_names = "\n".join([str(i) for i in conn_acts])
sel = subprocess.run(dmenu_cmd(len(conn_acts), "CHOOSE CONNECTION TO DELETE:"),
capture_output=True,
check=False,
input=conn_names,
encoding=ENC,
env=ENV).stdout
if not sel.strip():
sys.exit()
action = [i for i in conn_acts if str(i) == sel.rstrip("\n")]
assert len(action) == 1, f"Selection was ambiguous: {str(sel)}"
action[0]()
LOOP.run()
def delete_cb(dev, res, data):
"""Notification if delete completed successfully
"""
if dev.delete_finish(res) is True:
notify(f"Deleted {dev.get_id()}")
else:
notify(f"Problem deleting {dev.get_id()}", urgency="critical")
LOOP.quit()
def set_new_connection(nm_ap, nm_pw, adapter):
"""Setup a new NetworkManager connection
Args: ap - NM.AccessPoint
pw - string
"""
nm_pw = str(nm_pw).strip()
profile = create_wifi_profile(nm_ap, nm_pw, adapter)
CLIENT.add_and_activate_connection_async(profile, adapter, nm_ap.get_path(),
None, verify_conn, profile)
LOOP.run()
def create_wifi_profile(nm_ap, password, adapter):
# pylint: disable=line-too-long
# noqa From https://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python/gi/add_connection.py
# noqa and https://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python/dbus/add-wifi-psk-connection.py
# pylint: enable=line-too-long
"""Create the NM profile given the AP and passphrase"""
ap_sec = ap_security(nm_ap)
profile = NM.SimpleConnection.new()
s_con = NM.SettingConnection.new()
s_con.set_property(NM.SETTING_CONNECTION_ID, ssid_to_utf8(nm_ap))
s_con.set_property(NM.SETTING_CONNECTION_UUID, str(uuid.uuid4()))
s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless")
profile.add_setting(s_con)
s_wifi = NM.SettingWireless.new()
s_wifi.set_property(NM.SETTING_WIRELESS_SSID, nm_ap.get_ssid())
s_wifi.set_property(NM.SETTING_WIRELESS_MODE, 'infrastructure')
s_wifi.set_property(NM.SETTING_WIRELESS_MAC_ADDRESS, adapter.get_permanent_hw_address())
profile.add_setting(s_wifi)
s_ip4 = NM.SettingIP4Config.new()
s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")
profile.add_setting(s_ip4)
s_ip6 = NM.SettingIP6Config.new()
s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")
profile.add_setting(s_ip6)
if ap_sec != "--":
s_wifi_sec = NM.SettingWirelessSecurity.new()
if "WPA" in ap_sec:
if "WPA3" in ap_sec:
s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT,
"sae")
else:
s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT,
"wpa-psk")
s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_AUTH_ALG,
"open")
s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_PSK, password)
elif "WEP" in ap_sec:
s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT,
"None")
s_wifi_sec.set_property(NM.SETTING_WIRELESS_SECURITY_WEP_KEY_TYPE,
NM.WepKeyType.PASSPHRASE)
s_wifi_sec.set_wep_key(0, password)
profile.add_setting(s_wifi_sec)
return profile
def verify_conn(client, result, data):
"""Callback function for add_and_activate_connection_async
Check if connection completes successfully. Delete the connection if there
is an error.
"""
try:
act_conn = client.add_and_activate_connection_finish(result)
conn = act_conn.get_connection()
if not all([conn.verify(),
conn.verify_secrets(),
data.verify(),
data.verify_secrets()]):
raise GLib.Error
notify(f"Added {conn.get_id()}")
except GLib.Error:
try:
notify(f"Connection to {conn.get_id()} failed",
urgency="critical")
conn.delete_async(None, None, None)
except UnboundLocalError:
pass
finally:
LOOP.quit()
def create_ap_list(adapter, active_connections):
"""Generate list of access points. Remove duplicate APs , keeping strongest
ones and the active AP
Args: adapter
active_connections - list of all active connections
Returns: aps - list of access points
active_ap - active AP
active_ap_con - active Connection
adapter
"""
aps = []
ap_names = []
active_ap = adapter.get_active_access_point()
aps_all = sorted(adapter.get_access_points(),
key=lambda a: a.get_strength(), reverse=True)
conns_cur = [i for i in CONNS if
i.get_setting_wireless() is not None and
conn_matches_adapter(i, adapter)]
try:
ap_conns = active_ap.filter_connections(conns_cur)
active_ap_name = ssid_to_utf8(active_ap)
active_ap_con = [active_conn for active_conn in active_connections
if active_conn.get_connection() in ap_conns]
except AttributeError:
active_ap_name = None
active_ap_con = []
if len(active_ap_con) > 1:
raise ValueError("Multiple connection profiles match"
" the wireless AP")
active_ap_con = active_ap_con[0] if active_ap_con else None
for nm_ap in aps_all:
ap_name = ssid_to_utf8(nm_ap)
if nm_ap != active_ap and ap_name == active_ap_name:
# Skip adding AP if it's not active but same name as active AP
continue
if ap_name not in ap_names:
ap_names.append(ap_name)
aps.append(nm_ap)
return aps, active_ap, active_ap_con, adapter
def notify(message, details=None, urgency="low"):
"""Use notify-send if available for notifications
"""
delay = CONF.getint('nmdm', 'rescan_delay', fallback=5)
args = ["-u", urgency, "-a", "networkmanager-dmenu",
"-t", str(delay * 1000), message]
if details is not None:
args.append(details)
if is_installed("notify-send"):
subprocess.run(["notify-send"] + args, check=False)
def run(): # pylint: disable=too-many-locals
"""Main script entrypoint"""
active = CLIENT.get_active_connections()
adapter = choose_adapter(CLIENT)
if adapter:
ap_actions = create_ap_actions(*create_ap_list(adapter, active))
else:
ap_actions = []
vpns = [i for i in CONNS if i.is_type(NM.SETTING_VPN_SETTING_NAME)]
try:
wgs = [i for i in CONNS if i.is_type(NM.SETTING_WIREGUARD_SETTING_NAME)]
except AttributeError:
# Workaround for older versions of python-gobject with no wireguard support
wgs = []
eths = [i for i in CONNS if i.is_type(NM.SETTING_WIRED_SETTING_NAME)]
blues = [i for i in CONNS if i.is_type(NM.SETTING_BLUETOOTH_SETTING_NAME)]
vpn_actions = create_vpn_actions(vpns, active)
wg_actions = create_wireguard_actions(wgs, active)
eth_actions = create_eth_actions(eths, active)
blue_actions = create_blue_actions(blues, active)
other_actions = create_other_actions(CLIENT)
wwan_installed = is_installed("ModemManager")
if wwan_installed:
gsms = [i for i in CONNS if i.is_type(NM.SETTING_GSM_SETTING_NAME)]
gsm_actions = create_gsm_actions(gsms, active)
wwan_actions = create_wwan_actions(CLIENT)
else:
gsm_actions = []
wwan_actions = []
list_saved = CONF.getboolean('dmenu', 'list_saved', fallback=False)
saved_cons = [i for i in CONNS if i not in vpns + wgs + eths + blues]
if list_saved:
saved_actions = create_saved_actions(saved_cons)
else:
saved_actions = [Action("Saved connections", prompt_saved, [saved_cons])]
actions = combine_actions(eth_actions, ap_actions, vpn_actions, wg_actions,
gsm_actions, blue_actions, wwan_actions,
other_actions, saved_actions)
sel = get_selection(actions)
sel()
def main():
"""Main. Enables script to be re-run after a wifi rescan
"""
global CLIENT, CONNS, LOOP # noqa pylint: disable=global-variable-undefined
CLIENT = NM.Client.new(None)
LOOP = GLib.MainLoop()
CONNS = CLIENT.get_connections()
run()
if __name__ == '__main__':
main()
# vim: set et ts=4 sw=4 :

View File

@ -2,7 +2,7 @@
case $BUTTON in case $BUTTON in
1) notify-send "Memory hogs" "$(ps axch -o cmd:15,%mem --sort=-%mem | head)" ;; 1) notify-send "Memory hogs" "$(ps axch -o cmd:15,%mem --sort=-%mem | head)" ;;
2) setsid -f "$TERMINAL" -e htop ;; 2) setsid -f "$TERMINAL" -e gotop ;;
3) notify-send "Memory module" "Shows Memory Used/Total. 3) notify-send "Memory module" "Shows Memory Used/Total.
- Click to show memory hogs. - Click to show memory hogs.
- Middle click to open htop." ;; - Middle click to open htop." ;;

28
.local/bin/unmounter Executable file
View File

@ -0,0 +1,28 @@
#!/bin/sh
# Unmount USB drives or Android phones. Replaces the older `dmenuumount`. Fewer
# prompt and also de-decrypts LUKS drives that are unmounted.
set -e
mounteddroids="$(grep simple-mtpfs /etc/mtab | awk '{print "📱" $2}')"
lsblkoutput="$(lsblk -nrpo "name,type,size,mountpoint")"
mounteddrives="$(echo "$lsblkoutput" | awk '($2=="part"||$2="crypt")&&$4!~/\/boot|\/home$|SWAP/&&length($4)>1{printf "💾%s (%s)\n",$4,$3}')"
allunmountable="$(echo "$mounteddroids
$mounteddrives" | sed "/^$/d;s/ *$//")"
test -n "$allunmountable"
chosen="$(echo "$allunmountable" | dmenu -i -p "Unmount which drive?")"
chosen="${chosen%% *}"
test -n "$chosen"
sudo -A umount -l "/${chosen#*/}"
notify-send "Device unmounted." "$chosen has been unmounted."
# Close the chosen drive if decrypted.
cryptid="$(echo "$lsblkoutput" | grep "/${chosen#*/}$")"
cryptid="${cryptid%% *}"
test -b /dev/mapper/"${cryptid##*/}"
sudo -A cryptsetup close "$cryptid"
notify-send "🔒Device dencryption closed." "Drive is now securely locked again."