Ryan Rueger

ryan@rueg.re / picture / key / home
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md517
-rwxr-xr-xdesktopctl2065
2 files changed, 2582 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..544c6a7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,517 @@
+# desktopctl
+
+*"A desktop environment for window managers"*
+
+Using a window manager (i3/sway/awesome/etc) should not mean that one has to
+completely reinvent the wheel and manage a strange collection of one-liners to
+get wifi status or to see if headphones are connected. This project aims to be
+a solution to this problem.
+
+Some of these tools are simple one-liners, some are more complex. The idea is to
+create a suite of platform-independent (Distro/Desktop Environment independent)
+tools to get and set system information.
+
+Similar in nature to [neofetch](https://github.com/dylanaraps/neofetch), but
+also allows for *setting* information too (not just *getting*).
+
+### Project status
+
+The code is not particularly robust. It can and will fail in non-obvious ways if
+certain utility programs are not installed.
+
+Notably, this program assumes certain permissions are granted to the user. For
+example, to change the brightness in Archlinux, the user must be part of the
+`video` group; or to non-interactively change the time zone with `timedatectl`
+one needs a polkit rule.
+
+### Features
+
+* **Session Authentication** Show session authentication status
+* **Backlight Manager** Get and set backlight
+* **Battery Manager** Get battery status
+* **Bluetooth Manager** Get and set Bluetooth
+* **Colour picker** Colour picker
+* **CPU frequency** Get CPU frequency
+* **Headphone indicator** Get whether headpones are plugged in
+* **IP Address** Get local IP address
+* **Media Watch Time** Print total media time of listed files
+* **Memory** Get memory usage
+* **Monitor Directories/Files** Monitor directories/files
+* **Check if online** Get current internet connection status
+* **Screenshots** Take screenshots
+* **Sound Manager** Get and set sound
+* **Internet speedtest** Show current internet speed
+* **SSH** List ssh clients on local network
+* **Time zone** Get and set current time zone
+* **Wifi Status** Set wifi on/off
+
+Limited features
+
+* **Configuration file differences** Show configuration file differences (Archlinux only)
+* **Power Manager** Set power and charging profiles (Dell only)
+* **Reboot check** List programs that need to be restarted (Archlinux only)
+* **Xwayland clients** List number of xwayland clients (Sway only)
+
+---
+
+### Configuring
+
+The configuration is read from `~/.config/desktopctl/desktopctlrc`.
+
+---
+
+#### Session Authentication
+
+Show session
+
+Prints whether the session is [correctly
+authenticated](https://wiki.archlinux.org/title/General_troubleshooting#Session_permissions)
+using `loginctl`.
+
+Arguments: None
+
+#### Backlight Manager
+
+Get and set backlight
+
+Is configured to give non-linear brightness changes with a very simple
+interface. Can easily be bound to keyboard brightness keys.
+
+```
+Arguments:
+ NUMBER, Set backlight to NUMBER percentage.
+ +NUMBER, Increase backlight percentage by NUMBER.
+ -NUMBER, Decrease backlight percentage by NUMBER.
+
+ +, Go to the next backlight intensity level.
+ -, Go to the previous backlight intensity level.
+
+ -p, --print, Print current backlight percentages.
+ -n, --notify, Send desktop notification with current backlight intensity.
+ -h, --help, Display this help.
+```
+
+Changes are made with `xbacklight` under the hood.
+
+The brightness profile can be configured by setting `$BACKLIGHT_LEVELS` in the
+configuration file.
+
+The default is
+
+```
+BACKLIGHT_LEVELS=(0 1 2 3 4 5 6 7 8 9 10 15 20 25 30 40 50 60 70 80 90 100)
+```
+
+(Small steps at lower brightnesses and larger steps at higher brightnesses)
+
+#### Battery Manager
+
+Get battery status
+
+Gives very detailed statistics about the battery usage rate. Useful for building
+a status line. Comes shipped with a default status line.
+
+```
+Arguments:
+ power, Current power usage (in W)
+ voltage, Current battery voltage (in uV)
+ current, Current battery current (in uA)
+ capacity, Current battery capacity (in %)
+ charge, Current battery capacity (in C)
+ charging, Exit value 0 if charging
+ charged, Exit value 0 if fully charged
+
+ remaining, If on battery: remaining time until battery depleted
+ If charging: remaining time until battery full
+ (Human readable string)
+
+ info, Statusline like information about battery
+ (May need correct font configuration)
+```
+
+Configure battery icons with `$BATTERY_LEVELS_CHARGING` and `$BATTERY_LEVELS`.
+
+The defaults are set using [Overpass Mono Nerd
+Fonts](https://github.com/ryanoasis/nerd-fonts).
+
+```
+BATTERY_LEVELS_CHARGING=(󰂆 󰂆 󰂇 󰂈 󰂉 󰂊 󰂊 󰂋 󰂋 󰂅 )
+BATTERY_LEVELS=(󰁺 󰁻 󰁼 󰁽 󰁾 󰁿 󰂀 󰂁 󰂂 󰁹 )
+```
+
+(You may not be able to see these in browser)
+
+#### Bluetooth Manager
+
+Get and set Bluetooth
+
+```
+Arguments:
+ on, Power bluetooth on
+ off, Power bluetooth off
+ --daemon, daemon, Interact with the bluetooth daemon
+ -i, --indicator, Print current bluetooth connection indicator
+ -l, --list, list, List connected bluetooth devicees
+ -r, --restart, restart, Restart bluetooth
+ -c, --connect, connect, Connect to bluetooth device
+ -d, --disconnect, disconnect, Connect to bluetooth device
+ -h, --help, help, Print this help
+```
+
+To display battery status of bluetooth devices, experimental features must be
+enabled in `/etc/bluetooth/main.conf` in the `[General]` section by setting
+
+```
+Experimental = true
+```
+
+Allows configuring bluetooth from the command line whilst aliasing devices with
+nice names. For example
+
+```
+desktopctl bluetooth connect sony-headphones
+```
+
+Instead of
+
+```
+bluetooth on
+bluetoothctl agent on
+bluetoothctl default-agent
+# Find out the MAC address of your Bluetooth device
+# Suppose it is 00:23:02:C0:A9:6F
+bluetoothctl connect 00:23:02:C0:A9:6F
+```
+
+These aliases are defined in the configuration file with a function which must
+be called `desktopctl.bt.config` and should return from an alias the correct MAC
+address.
+
+Here is an example
+
+```
+# Inside ~/.config/desktopctl/desktopctlrc
+desktopctl.bt.config () {
+ case "$1" in
+ aukey) ID=00:23:02:C0:A9:6F ;;
+ soundcore) ID=08:EB:ED:5F:37:28 ;;
+ dell-keyboard) ID=ED:DD:9A:5F:57:DB ;;
+ dell-mouse) ID=EE:79:C0:96:C5:C3 ;;
+ hr) ID=DA:80:4B:56:CE:4E ;;
+ sony) ID=38:18:4C:BE:BB:00 ;;
+ jlab) ID=00:23:10:0C:74:47 ;;
+ wacom) ID=DC:2C:26:09:C7:70 ;;
+ shokz) ID=C0:86:B3:57:39:67 ;;
+ *) ID=$1 ;;
+ esac
+}
+````
+
+Bluetooth is also quite fragile. Often one needs to restart to make things work.
+This script handles this automatically.
+
+May require polkit rule to allow restarting bluetooth without password
+
+This rule will allow any user in the `wheel` group to restart the systemd
+`bluetooth.service`.
+
+```
+polkit.addRule(function(action, subject) {
+ if (action.id == "org.freedesktop.systemd1.manage-units" &&
+ subject.isInGroup("wheel") &&
+ action.lookup("unit") == "bluetooth.service") {
+ return polkit.Result.YES;
+ };
+});
+```
+
+#### Colour picker
+
+Simple colour picker. Copies value to clipboard in RBG, Hex and SRGB values.
+
+Compatible with wayland and X11.
+
+#### CPU frequency
+
+Prints `<avg CPU clock speed>/<max CPU clock speed>` in human readable form
+
+Data as reported by: `/sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq`
+
+```
+Arguments:
+ -a, --avg, avg, Only print average CPU clock speed over all cores
+ -m, --max, max, Only print maximum CPU clock speed over all cores
+ -r, --raw, raw, Do not make human readable
+
+ -h, --help, help, Display this help
+```
+
+Useful for status lines.
+
+#### Headphone indicator
+
+Prints indicator if headphones are plugged in
+
+Uses `pactl` under the hood
+
+Configure the icon in the configuration file by setting `HEADPHONE_INDICATOR`.
+
+Useful for status lines.
+
+#### IP Address
+
+Get local IP address.
+
+#### Media Watch Time
+
+Prints watch times of media with remaining time and percentage.
+
+Requires `column` from `util-linux`.
+
+```
+Arguments:
+ A list of media files (mp4)
+```
+
+Example:
+
+```
+$ cd tv-shows/planet-earth
+$ desktopctl mediawatchtime *mp4
+Item | Title | Duration | Watched | % Watched | Remaining
+1 | S01E01-planet-earth-from-pole-to-pole.mp4: | 0:59 | 0:59 | 9.00 | 9:44
+2 | S01E02-planet-earth-mountains.mp4: | 0:57 | 1:56 | 18.00 | 8:47
+3 | S01E03-planet-earth-fresh-water.mp4: | 0:58 | 2:55 | 27.00 | 7:48
+4 | S01E04-planet-earth-caves.mp4: | 0:57 | 3:52 | 36.00 | 6:51
+5 | S01E05-planet-earth-deserts.mp4: | 0:57 | 4:50 | 45.00 | 5:53
+6 | S01E06-planet-earth-ice-worlds.mp4: | 0:58 | 5:49 | 54.00 | 4:54
+7 | S01E07-planet-earth-great-plains.mp4: | 0:58 | 6:48 | 63.00 | 3:55
+8 | S01E08-planet-earth-jungles.mp4: | 0:58 | 7:46 | 72.00 | 2:56
+9 | S01E09-planet-earth-shallow-seas.mp4: | 0:58 | 8:45 | 81.00 | 1:57
+10 | S01E10-planet-earth-seasonal-forests.mp4: | 0:58 | 9:44 | 90.00 | 0:58
+11 | S01E11-planet-earth-ocean-deep.mp4: | 0:58 | 10:43 | 100.00 | 0:00
+```
+
+#### Memory
+
+Print memory usage (human readable).
+
+```
+Options
+ -r, --raw, raw, Display in raw bytes (not human readable).
+```
+
+Useful for status lines
+
+#### Monitor Directories/Files
+
+Prints size, number of files in directory, and changes since last polling.
+
+Argument: Directory or file to monitor
+
+```
+Arguments:
+ dir, Directory or file to monitor
+ -i, --interval, Time interval to re-compute file sizes and changes in seconds
+ (Default 60)
+```
+
+E.g.
+
+```
+$ rsync /path/to/src/dir /path/to/dest/dir
+```
+
+Forgot to add progress options! ... instead of cancelling the transfer, just
+monitor the destination directory directly
+
+```
+$ desktopctl mondir -i 5 /path/to/dest/dir
+mondir: Monitoring '/path/to/dest/dir' with interval 5s.
+
+2024-03-30-15:23:14 Total: 188G Files: 349222
+2024-03-30-15:23:20 Total: 188G Files: 349266 Diff: 500MiB Files diff: 44 Speed: 100MiB/s
+2024-03-30-15:23:26 Total: 189G Files: 349339 Diff: 525MiB Files diff: 73 Speed: 105MiB/s
+...
+```
+
+Of course this is also useful for tools that do not offer progress statistics.
+
+#### Check if online
+
+Get current internet connection status
+
+```
+Arguments:
+ -t, --timeout, Set timeout in seconds
+ -q, --quiet, Do not print. Only uses exit value
+```
+
+Note: Options must be specified separately i.e. `-q -t 30` and not `-qt30` or
+`-qt 30`.
+
+Useful in scripts, to start backups or synchronise emails only when connected to
+the internet.
+
+#### Screenshots
+
+Take screenshots
+
+Supports taking a partial selection of the screen
+
+Supports wayland/x11
+
+Screenshots are copied to the clipboard
+
+```
+Arguments:
+ --select, Will ask user to make selection of area to screenshot
+ --blurred, (x11 only) Will blur screenshot
+```
+
+Blurred screenshots are nice for creating a lockscreen with current screen
+contents blurred.
+
+#### Sound Manager
+
+Get and set sound
+
+```
+Arguments:
+ +, Increase volume
+ -, Decrease volume
+ sink, Print current default output sink
+ ismuted, Print "yes" if muted all devices are muted, else no
+ notify, Send a desktop notification of current volume
+ get-volume, get Print current volume
+ mute, Mute current default sink
+ toggle, Toggle mute status
+```
+
+Uses `pactl` under the hood
+
+Useful for displaying current volume in status line or when binding keyboard
+buttons.
+
+#### Internet speedtest
+
+Show current internet speed
+
+`curl`s a file from Linode
+
+```
+http://eu-central-1.linodeobjects.com/speedtest/100MB-speedtest
+```
+
+#### SSH
+
+List ssh clients on local network
+
+Useful for finding a headless Raspberry Pi after setting it up.
+
+#### Time zone
+
+Get and set current time zone.
+
+Uses [ip-api.com](http://ip-api.com/) to get the time zone based on IP address.
+
+Uses systemd's `timedatectl` to set the time zone. To do this non-interactively
+one needs the following polkit rule
+
+```
+# /etc/polkit-1/rules.d/timedate.rules
+polkit.addRule(function(action, subject) {
+ if (action.id == "org.freedesktop.timedate1.set-time") {
+ return polkit.Result.YES;
+ }
+});
+```
+
+#### Wifi Status
+
+Set wifi on/off. Show wifi connection status, signal strength. Useful for status
+lines.
+
+```
+Arguments:
+ ssid, Print connected ssid
+ icons, Print ssid with icons
+ only-icons Only print icons
+ off, Turn wifi off
+ on, Turn wifi on
+```
+
+Configuration values
+
+```
+WIFI_ICON_CONFIGURING=󰤫
+WIFI_ICON_LOW_STRENGTH=󰤟
+WIFI_ICON_MED_STRENGTH=󰤢
+WIFI_ICON_HIG_STRENGTH=󰤨
+WIFI_ICON_DISCONNECTED=󰤯
+WIFI_ICON_OFF=󰤮
+# nm for Network Manager and iwd for IWD
+WIFI_BACKEND=nm
+WIFI_DEVICE=wlan0
+```
+
+---
+
+### Limited Features
+
+#### Config Diff
+
+Uses configured `DIFFTOOL` (from configuration file) to display changes of files
+that have been modified on disk from how they were originally shipped with the
+package manager.
+
+```
+Arguments:
+ path, Path to configuration file
+```
+
+Example: See changes from the default in the bluetooth configuration file, call
+
+```
+desktopctl config_diff /etc/bluetooth/main.conf
+```
+
+Only works on Archlinux.
+
+#### Power Manager
+
+```
+Charging Profiles:
+ ac, Primarily AC Charging, Auto performance
+ express, Express charging, Low performance
+
+Performance:
+ auto, Automatically adjust performance
+ cool, Set performance profile to cool
+ desktop, Highest performance settings
+ laptop, Laptop performance settings
+ low, Set perforamnce to power saving
+ quiet, Set performance profile to quiet
+```
+
+#### Reboot check
+
+Prints information about what services need to be restarted, and whether the
+kernel is old
+
+Used to generally inform, whether the system should be rebooted
+
+Can be wrapped with pacman hook to give information after update
+
+```
+Options:
+ -d, --detailed, Print more detailed information
+```
+
+#### Xwayland clients
+
+Count xwayland clients
+
+Arguemnts:
+ format, A format string in which '%n' will replace number of Xwayland clients
+ if there are any. If there are none, nothing will be printed.
diff --git a/desktopctl b/desktopctl
new file mode 100755
index 0000000..bac460b
--- /dev/null
+++ b/desktopctl
@@ -0,0 +1,2065 @@
+#!/bin/bash
+
+# dsektopctl
+#
+# A desktop environment for window managers
+#
+# Ryan Rueger 2024
+
+shopt -s extglob
+
+# Configuration {{{{{
+if [[ -r "$HOME/.config/desktopctl/desktopctlrc" ]]
+then
+ source "$HOME/.config/desktopctl/desktopctlrc"
+fi
+
+# Battery icons
+if [[ -z BATTERY_LEVELS_CHARGING ]]
+then
+ BATTERY_LEVELS_CHARGING=(󰂆 󰂆 󰂇 󰂈 󰂉 󰂊 󰂊 󰂋 󰂋 󰂅 )
+fi
+
+if [[ -z BATTERY_LEVELS ]]
+then
+ BATTERY_LEVELS=(󰁺 󰁻 󰁼 󰁽 󰁾 󰁿 󰂀 󰂁 󰂂 󰁹 )
+fi
+
+# Window manager
+WINDOWING=${XDG_SESSION_TYPE:-x11}
+XAUTHORITY=${XAUTHORITY:-$HOME/.Xauthority}
+
+# Screenshots
+SCREENSHOTS=${SCREENSHOTS:-$HOME/.cache/screenshots}
+SCREENSHOT_SIZE=${SCREENSHOT_SZE:-1920x1080}
+
+# General
+DIFFTOOL=${DIFFTOOL:-diff}
+
+# Wifi configuration
+WIFI_ICON_OFF=${WIFI_ICON_OFF:-Off}
+WIFI_ICON_ON=${WIFI_ICON_ON:-Connected:}
+WIFI_BACKEND=${WIFI_BACKEND:-nm}
+WIFI_DEVICE=${WIFI_DEVICE:-wlan0}
+
+# Headphone indicator
+HEADPHONE_INDICATOR=${HEADPHONE_INDICATOR:-"H"}
+
+# Power statistics
+UEVENT=${UEVENT:-/sys/class/power_supply/BAT0/uevent}
+POWERSTATS=${POWERSTATS:-$HOME/.powerstats}
+
+# Backlight power levels
+# Cannot use default values syntax for arrays
+if [[ -z $BACKLIGHT_LEVELS ]]
+then
+ BACKLIGHT_LEVELS=(0 1 2 3 4 5 6 7 8 9 10 15 20 25 30 40 50 60 70 80 90 100)
+fi
+
+# Bluetooth
+BLUETOOTH_ICON_ON=${BLUETOOTH_ICON_ON:-B}
+BLUETOOTH_ICON_CONNECTED=${BLUETOOTH_ICON_CONNECTED:-Bc}
+
+# export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$UID/bus
+# export DISPLAY=:0
+# }}}}}
+
+# Logging {{{{{
+desktopctl.log () {
+ 2>&1 echo "desktopctl: $*"
+}
+# }}}}}
+
+# Dependencycheck {{{{{
+desktopctl.dependencycheck () {
+ for DEPENDENCY in "$@"
+ do
+ if ! command -v "$DEPENDENCY" > /dev/null 2>&1
+ then
+ desktopctl.log "dependencycheck: Requires executable '$DEPENDENCY'"
+ exit 2
+ fi
+ done
+}
+# }}}}}
+# Sudocheck {{{{{
+desktopctl.sudocheck () {
+ for COMMAND in "$@"
+ do
+ if ! sudo -n "$@" > /dev/null 2>&1
+ then
+ desktopctl.log "sudocheck: Error: You don't have sufficient permissions to execute '$COMMAND'."
+ fi
+done
+}
+# }}}}}
+# Rounding calculator {{{{{
+desktopctl.bcr () {
+ bc -l <<EOF
+t=$1
+scale=${2:-2}
+
+(t*10^scale+((t>0)-(t<0))/2)/10^scale
+EOF
+}
+# }}}}}
+
+# Session Authentication {{{{{
+desktopctl.session_auth.usage () {
+cat <<EOF
+[desktopctl] Session authentiaction check
+
+Prints whether the session is correctly authenticated using loginctl
+
+Arguments: None
+
+Reference: https://wiki.archlinux.org/title/General_troubleshooting#Session_permissions
+EOF
+}
+
+desktopctl.session_auth.check () {
+ # This is rather crude
+ # In theory this should be able to detect when a session is not properly
+ # authenticated, but it is difficult to use this in practice: in practice one
+ # would like a notification to somehow warn the user that their session is not
+ # properly authenticated. For example, this script is started by a user
+ # systemd unit after login: but since this often runs in a different
+ # environmental context, the loginctl will tell the background script that
+ # things are okay, when running the script from the terminal will tell you
+ # that they're not.
+ #
+ # https://wiki.archlinux.org/title/General_troubleshooting#Session_permissions
+
+ if [[ $(loginctl show-session --property=Active --property=Remote self | sort | paste -sd' ') != 'Active=yes Remote=no' ]]
+ then
+ if [[ $1 != -q ]]
+ then
+ echo "Session *not* properly authenticated"
+ fi
+ exit 1
+ else
+ if [[ $1 != -q ]]
+ then
+ echo "desktopctl: Session properly authenticated"
+ fi
+ fi
+}
+
+desktopctl.session_auth () {
+ case "$1" in
+ '') desktopctl.session_auth.check ;;
+ *) desktopctl.session_auth.usage ;;
+ esac
+}
+# }}}}}
+# Backlight {{{{{
+desktopctl.backlight.usage () {
+cat <<EOF
+[desktopctl] Backlight Management
+
+Arguments:
+ NUMBER, Set backlight to NUMBER percentage.
+ +NUMBER, Increase backlight percentage by NUMBER.
+ -NUMBER, Decrease backlight percentage by NUMBER.
+
+ +, Go to the next backlight intensity level.
+ -, Go to the previous backlight intensity level.
+
+ -p, --print, Print current backlight percentages.
+ -n, --notify, Send desktop notification with current backlight intensity.
+ -h, --help, Display this help.
+
+Can be configured by setting BACKLIGHT_LEVELS in configuration file
+
+Default is
+BACKLIGHT_LEVELS=(0 1 2 3 4 5 6 7 8 9 10 15 20 25 30 40 50 60 70 80 90 100)
+
+Uses 'xbacklight' under the hood.
+EOF
+}
+
+desktopctl.backlight.notify () {
+ notify-send \
+ "backlight: $(desktopctl.backlight.get)" \
+ -t 5000 \
+ -h string:x-canonical-private-synchronous:backlight
+}
+
+desktopctl.backlight.get () {
+ # Get current backlight intensity as a percentage
+ #
+ # Arguments: None
+
+ xbacklight -get
+}
+
+desktopctl.backlight.set () {
+ # Set the backlight to a given intensity percentage
+ #
+ # Arguments:
+ # (1) Backlight intensity - Number between 0-100 (inclusive)
+
+ xbacklight -set "$1"
+}
+
+desktopctl.backlight.inc () {
+ # Increase the backlight intensity by a given number of percentage points
+ #
+ # Arguments:
+ # (1) Backlight intensity increase - Number between 0-100 (inclusive)
+
+ xbacklight -inc "$1"
+}
+
+desktopctl.backlight.dec () {
+ # Decrease the backlight intensity by a given number of percentage points
+ #
+ # Arguments:
+ # (1) Backlight intensity decrease - Number between 0-100 (inclusive)
+
+ xbacklight -dec "$1"
+}
+
+desktopctl.backlight.get_level () {
+ # Get current backlight intensity as an intensity level
+ #
+ # Arguments: None
+
+ # Current backlight intensity
+ CURRENT=$(desktopctl.backlight.get)
+
+ # Iter over all level intervals, to see in which level interval the current
+ # brightness falls
+
+ level=0
+ for level in "${!BACKLIGHT_LEVELS[@]}"
+ do
+ if (( CURRENT == ${BACKLIGHT_LEVELS[$level]} )) || \
+ (\
+ (( ${BACKLIGHT_LEVELS[$level]} < CURRENT )) \
+ && \
+ (( CURRENT < ${BACKLIGHT_LEVELS[$((level+1))]} ))
+ )
+ then
+ echo "$level"
+ fi
+ done
+}
+
+desktopctl.backlight.set_level () {
+ # Set current backlight intensity as an intensity level
+ #
+ # Arguments:
+ # (1) Backlight intensity level - Number between 0-20 (inclusive)
+
+ if [[ ${BACKLIGHT_LEVELS[$1]} =~ ^[0-9]+$ ]]
+ then
+ desktopctl.backlight.set "${BACKLIGHT_LEVELS[$1]}"
+ else
+ desktopctl.log "backlight: Error '$1' is not one of the specified levels: '${!BACKLIGHT_LEVELS[*]}'"
+ fi
+}
+
+
+desktopctl.backlight.inc_level () {
+ # Increment the backlight intensity to the next intensity level
+ #
+ # Arguments: None
+
+ CURRENT=$(desktopctl.backlight.get_level)
+
+ if (( CURRENT != ${#BACKLIGHT_LEVELS[@]} - 1 ))
+ then
+ desktopctl.backlight.set_level "$((CURRENT+1))"
+ fi
+}
+
+desktopctl.backlight.dec_level () {
+ # Decrement the backlight intensity to the previous intensity level
+ #
+ # Arguments: None
+
+ CURRENT=$(desktopctl.backlight.get_level)
+
+ if (( CURRENT != 0 ))
+ then
+ desktopctl.backlight.set_level "$((CURRENT-1))"
+ fi
+}
+
+desktopctl.backlight () {
+ desktopctl.dependencycheck acpi
+
+ case $1 in
+ +) desktopctl.backlight.inc_level && desktopctl.backlight.notify ;;
+ -) desktopctl.backlight.dec_level && desktopctl.backlight.notify ;;
+ ++([0-9])) desktopctl.backlight.inc "${1#+}" && desktopctl.backlight.notify ;;
+ -+([0-9])) desktopctl.backlight.dec "${1#-}" && desktopctl.backlight.notify ;;
+ +([0-9])) desktopctl.backlight.set "$1" && desktopctl.backlight.notify ;;
+ -p|--print) desktopctl.backlight.get ;;
+ -h|--help) desktopctl.backlight.usage ;;
+ ''|-n|--notify) desktopctl.backlight.notify ;;
+ *) desktopctl.backlight.usage ;;
+ esac
+}
+# }}}}}
+# Battery {{{{{
+desktopctl.battery.usage () {
+cat <<EOF
+[desktopctl] Battery usage
+
+Arguments:
+ power, Current power usage (in W)
+ voltage, Current battery voltage (in uV)
+ current, Current battery current (in uA)
+ capacity, Current battery capacity (in %)
+ charge, Current battery capacity (in C)
+ charging, Exit value 0 if charging
+ charged, Exit value 0 if fully charged
+
+ remaining, If on battery: remaining time until battery depleted
+ If charging: remaining time until battery full
+ (Human readable string)
+
+ info, Statusline like information about battery
+ (May need correct font configuration)
+
+ Remaining battery is polled from acpi.
+ It should be roughly equivalent to the value of capacity/current as output by
+ desktopctl.
+
+Requires acpi
+EOF
+}
+
+desktopctl.battery.get_voltage () {
+ # Get the voltage (in uV) currently being drawn from battery as reported by
+ # UEVENT
+ #
+ # Arguments: None
+
+ # awk -F= '$1 == "POWER_SUPPLY_VOLTAGE_NOW" {print $2}' "$UEVENT"
+ cat /sys/class/power_supply/BAT0/voltage_now
+}
+
+desktopctl.battery.get_current () {
+ # Get the current (in uAmps) currently being drawn from the battery as
+ # reported by UEVENT
+ #
+ # Arguments: None
+
+ # awk -F= '$1 == "POWER_SUPPLY_CURRENT_NOW" {print $2}' "$UEVENT"
+ cat /sys/class/power_supply/BAT0/current_now
+}
+
+desktopctl.battery.calc_power () {
+ # Calculate the current power consumption in watts (W)
+ #
+ # Arguments: None
+
+ # Since the voltage and current are given in uV and uA we must divide by
+ # 10^6 twice to obtin a result in watts.
+ # bc -l <<< "scale=2; $(desktopctl.battery.get_voltage) * $(desktopctl.battery.get_current) / 10^12"
+ if cat /sys/class/power_supply/BAT0/{current_now,voltage_now} > /dev/null 2>&1
+ then
+ bc -l <<< "scale=1; $(paste -d'*' /sys/class/power_supply/BAT0/{current_now,voltage_now})/10^12"
+ else
+ echo ??
+ fi
+}
+
+desktopctl.battery.get_capacity () {
+ # Get the current battery capacity from the UEVENT file as a percentage of
+ # the last full charge
+ #
+ # Arguments: None
+
+ awk -F= '$1 == "POWER_SUPPLY_CAPACITY" {print $2}' "$UEVENT"
+}
+
+desktopctl.battery.get_charge () {
+ # Get the total charge (in coulombs) that the battery currently contains as
+ # reported by the UEVENT file
+ #
+ # Arguments: None
+
+ awk -F= '$1 == "POWER_SUPPLY_CHARGE_NOW" {print $2}' "$UEVENT"
+}
+
+desktopctl.battery.is_charging () {
+ # True if battery is charging (but not full), false otherwise.
+ #
+ # Arguments: None
+
+ grep -qF Charging "$UEVENT"
+}
+
+desktopctl.battery.is_charged () {
+ # True if battery is full, false otherwise.
+ #
+ # Arguments: None
+ grep -qF Charging "$UEVENT"
+}
+
+desktopctl.battery.remaining () {
+ # Return (in human readable format) the current time remaining time left as
+ # calculated by acpi
+ # If charging, the time until the battery is full will be displayed
+ #
+ # Arguments: None
+
+ acpi -b | awk '{print $5}' | awk -F: '{print $1 ":" $2}'
+}
+
+desktopctl.battery.info () {
+ bcr () {
+ # Rounding calculator
+ bc -l <<< "t=$1; scale=${2:-2}; (t*10^scale+((t>0)-(t<0))/2)/10^scale"
+ }
+
+ get_hist () {
+ # Crude check, to see if the battery is currently properly understood by the
+ # kernel
+ # (After plugging a laptop in, and waiting for it to charge, there can be a
+ # few seconds, where everything is confused --- noticable on usb-c machines
+ # that take 10-15 seconds to negotiate charging voltages/currents with the
+ # charging block)
+ if cat /sys/class/power_supply/BAT0/{current_now,voltage_now} > /dev/null 2>&1
+ then
+ # Read file backwards, because we want the most recent entries
+ tac "$POWERSTATS" | awk -v n=$(($1*2)) '{if ($2 !=0) {c+=$2;s+=1}; if (s==n) {printf "%.1f\n", c/n; exit}}'
+ else
+ echo ??
+ fi
+ }
+
+ PERCENTAGE=$(cat /sys/class/power_supply/BAT0/capacity)
+ BATTERY_STATUS=$(cat /sys/class/power_supply/BAT0/status)
+
+ if [[ "$BATTERY_STATUS" == "Full" ]]
+ then
+ ICON=""
+ elif [[ "$BATTERY_STATUS" == "Charging" ]]
+ then
+ ICON="${BATTERY_LEVELS_CHARGING[$((PERCENTAGE/10))]}"
+
+ BATTERY_INFO="${PERCENTAGE}%${ICON} $(desktopctl.battery.calc_power)W"
+ else
+ ICON="${BATTERY_LEVELS[$((PERCENTAGE/10))]}"
+
+ NOW=$(get_hist 5)
+ SHORT_TERM=$(get_hist 600)
+ BATTERY_INFO="${NOW}/${SHORT_TERM} ${PERCENTAGE}% ${ICON}"
+ fi
+
+ echo "$BATTERY_INFO" | sed 's/\s*$//'
+}
+
+desktopctl.battery.daemon () {
+ while true
+ do
+ TIME=$(date '+%F-%H%M%S.%N')
+ POWER=$(_power)
+ echo "$TIME $POWER" >> "$POWERSTATS"
+ sleep 1
+ done
+}
+
+desktopctl.battery () {
+ if [[ ! -r "$UEVENT" ]] ; then
+ desktopctl.log "stats: Error: Cannot read uevent file '$UEVENT'"
+ exit 1
+ fi
+
+ case "$1" in
+ voltage) desktopctl.battery.get_voltage ;;
+ current) desktopctl.battery.get_current ;;
+ power) desktopctl.battery.calc_power ;;
+ capacity) desktopctl.battery.get_capacity ;;
+ charge) desktopctl.battery.get_charge ;;
+ charging) desktopctl.battery.is_charging ;;
+ charged) desktopctl.battery.is_charged ;;
+ remaining) desktopctl.battery.remaining ;;
+ info) desktopctl.battery.info ;;
+ *) desktopctl.battery.usage ;;
+ esac
+}
+# }}}}}
+# Bluetooth {{{{{
+desktopctl.bt.usage () {
+cat <<EOF
+[desktopctl] Bluetooth management
+
+Arguments:
+ on, Power bluetooth on
+ off, Power bluetooth off
+ --daemon, daemon, Interact with the bluetooth daemon
+ -i, --indicator, Print current bluetooth connection indicator
+ -l, --list, list, List connected bluetooth devicees
+ -r, --restart, restart, Restart bluetooth
+ -c, --connect, connect, Connect to bluetooth device
+ -d, --disconnect, disconnect, Connect to bluetooth device
+ -h, --help, help, Print this help
+
+To display battery status of bluetooth devices, experimental features must be
+enabled in /etc/bluetooth/main.conf in the [General] section by setting
+
+ Experimental = true
+EOF
+}
+
+desktopctl.bt.is_powered () {
+ if [[ $1 == -v ]]
+ then
+ if desktopctl.bt.is_powered
+ then
+ echo "bt: Bluetooth is powered on"
+ # Do not mask the return value of desktopctl.bt.is_powered
+ true
+ else
+ echo "bt: Bluetooth is powered off"
+ # Do not mask the return value of desktopctl.bt.is_powered
+ false
+ fi
+ else
+ [[ $(bluetoothctl show | awk '/Powered/ {print $2}') == yes ]]
+ fi
+}
+
+desktopctl.bt.power_off () {
+ # Uses rfkill under the hood
+ # 'bluetooth' command is supplied by package 'tlp'
+ bluetooth off > /dev/null 2>&1
+}
+
+desktopctl.bt.power_on () {
+ sudo systemctl start bluetooth
+ # Uses rfkill under the hood
+ # 'bluetooth' command is supplied by package 'tlp'
+ bluetooth on > /dev/null 2>&1
+ sleep 0.5
+ if ! bluetoothctl power on > /dev/null 2>&1
+ then
+ echo "desktopctl: bt: Bluez not ready"
+ fi
+}
+
+desktopctl.bt.list_connected_devices () {
+ if [[ $1 == '-1' ]]
+ then
+ if ! desktopctl.bt.is_powered
+ then
+ exit 1
+ fi
+ bluetoothctl devices \
+ | awk '{print $2}' \
+ | while read -r UUID; do bluetoothctl info "$UUID"; done \
+ | awk '/Name|Connected|Battery/ {print $2}' \
+ | paste - - - \
+ | awk '$2 == "yes" {print $1}' \
+ | paste -sd' '
+ else
+ if ! desktopctl.bt.is_powered -v
+ then
+ exit 1
+ fi
+ echo
+ bluetoothctl devices \
+ | awk '{print $2}' \
+ | while read -r UUID; do bluetoothctl info "$UUID"; done \
+ | awk '/Name|Connected/ {print $2}' \
+ | paste - - \
+ | column -t -N Device,Connected \
+ | sed 's/^/ /'
+
+ echo
+ fi
+}
+
+desktopctl.bt.restart () {
+ echo -n "Restarting bluetooth ..."
+ systemctl restart bluetooth --quiet
+ echo -e "\r[Done] Restarting bluetooth."
+
+ # Unblock bluetooth availablility
+ desktopctl.bt.power_on
+
+ echo -n "Powering on bluetooth radio..."
+ bluetoothctl power on > /dev/null 2>&1
+ echo -e "\r[Done] Powering on bluetooth radio."
+
+ echo -n "Starting agent..."
+ bluetoothctl agent on > /dev/null 2>&1
+ bluetoothctl default-agent > /dev/null 2>&1
+ echo -e "\r[Done] Starting agent..."
+}
+
+desktopctl.bt.daemon () {
+ # Automatically turn off bluetooth if no devices are connected for 5 minutes
+
+ if (($(pgrep -u "$UID" -cxf '^.*/de bt --daemon.*') > 1))
+ then
+ echo "bt: Daemon already running."
+ exit 1
+ fi
+
+ NOT_CONNECTED=0
+ THRESHOLD=5
+ NOTIFIED=false
+
+ while true
+ do
+ if desktopctl.bt.is_powered
+ then
+ if (($(desktopctl.bt.list_connected_devices | awk 'c=0; $2 == "yes" {c++} END {print c}') == 0))
+ then
+ ((NOT_CONNECTED++))
+
+ if ((NOT_CONNECTED > "$THRESHOLD"))
+ then
+ echo "bt: Powering off bluetooth after $THRESHOLD minutes inactivity"
+ desktopctl.bt.power_off
+ notify "desktopctl: bt: Powering off bluetooth"
+ NOT_CONNECTED=0
+ fi
+ NOTIFIED=false
+ else
+ NOT_CONNECTED=0
+ if [[ $NOTIFIED == false ]]
+ then
+ echo "t: Bluetooth is powered on again, and connected to a device"
+ fi
+ NOTIFIED=true
+ fi
+ else
+ NOT_CONNECTED=0
+ NOTIFIED=false
+ fi
+
+ sleep 60
+ done &
+ exit
+}
+
+desktopctl.bt.connect () {
+ desktopctl.bt.config "$1"
+
+ echo -n "Connecting to device..."
+
+ attempt=1
+ until grep -q 'Connected: yes' <(bluetoothctl info "$ID")
+ do
+ if ((attempt % 10 == 0))
+ then
+ desktopctl.bt.restart
+ else
+ echo -en "\rConnection attempt $attempt"
+ bluetoothctl connect "$ID" > /dev/null 2>&1
+ sleep 1
+ ((attempt++))
+ fi
+ done
+ # Need trailing spaces to clean up the
+ # Connecting to device...
+ # string
+ echo -e "\rConnected "
+}
+
+desktopctl.bt.disconnect () {
+ desktopctl.bt.config "$1"
+ bluetoothctl disconnect "$ID"
+}
+
+desktopctl.bt.indicator () {
+ if (( $(desktopctl.bt.list_connected_devices | grep -c 'yes$') != 0 ))
+ then
+ echo "$BLUETOOTH_ICON_CONNECTED"
+ elif desktopctl.bt.is_powered
+ then
+ echo "$BLUETOOTH_ICON_ON"
+ else
+ echo ""
+ fi
+}
+
+desktopctl.bt () {
+ case "$1" in
+ on) shift; desktopctl.bt.power_on ;;
+ off) shift; desktopctl.bt.power_off ;;
+ --daemon|daemon) shift; desktopctl.bt.daemon ;;
+ -i|--indicator) shift; desktopctl.bt.indicator ;;
+ -l|--list|list) shift; desktopctl.bt.list_connected_devices "$@" ;;
+ -r|--restart|restart) shift; desktopctl.bt.restart ;;
+ -c|--connect|connect) shift; desktopctl.bt.power_on && desktopctl.bt.connect "$@" ;;
+ -d|--disconnect|disconnect) shift; desktopctl.bt.disconnect "$@" ;;
+ -h|--help|help|*) shift; desktopctl.bt.usage ;;
+ esac
+}
+# }}}}}
+# Colour picker {{{{{
+desktopctl.colour_picker.usage () {
+cat <<EOF
+[desktopctl] Colour picker
+
+Arguments: None
+
+If using wayland, requirements are
+ - grim
+ - imagemagick
+
+If using x11, requirements are
+ - setxkbmap
+ - xclip
+ - xcolor
+ - xdotool
+EOF
+}
+
+desktopctl.colour_picker.pick () {
+ if [[ $WINDOWING == wayland ]]
+ then
+ grim -g "$(slurp -p)" -t ppm - | convert - -format '%[pixel:p{0,0}]' txt:- | tail +2 | head -1 | cut -f2- -d' ' | wl-copy
+ else
+ # Sometimes the pointer is automatically hidden, when it is not moved
+ # Starting xcolor does not unhide it
+ # We need to manually unhide it so that we can see what we aree clicking
+ setxkbmap -option grab:break_actions
+ xdotool key XF86Ungrab
+ xcolor | xclip -selection clipboard
+ fi
+}
+
+desktopctl.colour_picker () {
+ case "$1" in
+ '') shift; desktopctl.colour_picker.pick "$@" ;;
+ *) shift; desktopctl.colour_picker.usage "$@" ;;
+ esac
+}
+# }}}}}
+# Config Diff {{{{{
+desktopctl.config_diff.usage () {
+cat <<EOF
+[desktopctl] System configuration files differences
+
+Uses configured \$DIFFTOOL to display changes of files that have been modified
+on disk from how they were originally shipped with the package manager.
+
+Arguments:
+ path, Path to configuration file
+
+Example: See changes from the default in the bluetooth configuration file, call
+
+$ desktopctl config_diff /etc/bluetooth/main.conf
+
+Only works on Archlinux.
+EOF
+}
+
+desktopctl.config_diff.diff () {
+ config=$1
+ if [[ ! -r $1 ]]
+ then
+ echo "desktopctl: Error: Cannot read file '$1'"
+ exit 1
+ fi
+ package_ver=$(pacman -Qo "$config" | awk '{printf "%s-%s\n",$(NF-1),$NF}')
+ if [[ -z $package_ver ]]
+ then
+ echo "desktopctl: Error: File '$1' is not owned by any package"
+ exit 1
+ fi
+ cache_filename=$(ls "/var/cache/pacman/pkg/$package_ver"*zst)
+ tar --to-stdout --extract "${config#/}" -f "$cache_filename" > /tmp/config
+ $DIFFTOOL "/tmp/config" "$config"
+}
+
+desktopctl.config_diff () {
+ if [[ ! -r $1 ]]
+ then
+ echo "desktopctl: Error: Cannot read file '$1'"
+ desktopctl.config_diff.usage
+ exit 1
+ else
+ desktopctl.config_diff.diff "$1"
+ fi
+}
+# }}}}}
+# CPU {{{{{
+
+desktopctl.cpu.usage () {
+cat <<EOF
+[desktopctl] CPU usage
+
+Prints '<avg CPU clock speed>/<max CPU clock speed>' in human readable form
+
+Data as reported by: '/sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq'
+
+Arguments:
+ -a, --avg, avg, Only print average CPU clock speed over all cores
+ -m, --max, max, Only print maximum CPU clock speed over all cores
+ -r, --raw, raw, Do not make human readable
+
+ -h, --help, help, Display this help
+EOF
+}
+
+desktopctl.cpu.avg () {
+ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq | awk '{total+=$1;n+=1} END {printf "%.0f\n", total/n}'
+}
+
+desktopctl.cpu.max () {
+ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq | sort -n | tail -1
+}
+
+desktopctl.cpu () {
+ case "$1" in
+ -h|--help|help) desktopctl.cpu.usage ;;
+ -r|--raw|raw) echo "$(desktopctl.cpu.avg)/$(desktopctl.cpu.max)" ;;
+ -a|--avg|avg) desktopctl.cpu.avg | numfmt --from-unit=1024 --to=iec --suffix=Hz ;;
+ -m|--max|max) desktopctl.cpu.max | numfmt --from-unit=1024 --to=iec --suffix=Hz ;;
+ '') echo "$(desktopctl.cpu.avg | numfmt --from-unit=1024 --to=iec --suffix=Hz)/$(desktopctl.cpu.max | numfmt --from-unit=1024 --to=iec --suffix=Hz)" ;;
+ *) desktopctl.cpu.usage ;;
+ esac
+}
+# }}}}}}
+# Headphone indicator {{{{{
+desktopctl.headphones.usage () {
+cat <<EOF
+[desktopctl] Headphones indicator
+
+Prints indicator if headphones are plugged in
+
+Arguments: None
+Options: None
+
+Uses pactl under the hood
+
+Configure the icon in the configuration file by setting HEADPHONE_INDICATOR.
+EOF
+}
+
+desktopctl.headphones.indicator () {
+ if grep -q '^\s*Active Port:.*headphones$' < <(pactl list sinks)
+ then
+ echo "$HEADPHONE_INDICATOR"
+ fi
+}
+
+desktopctl.headphones () {
+ case "$1" in
+ '') desktopctl.headphones.indicator "$@" ;;
+ *) desktopctl.headphones.usage "$@" ;;
+ esac
+}
+# }}}}}
+# Local ip {{{{{
+desktopctl.local_ip.usage () {
+cat <<EOF
+[desktopctl] Get local IP address
+
+No arguments.
+EOF
+}
+
+desktopctl.local_ip.get () {
+ ip route get 1 | sed -n 's/.*src \(\S*\) .*/\1/p'
+}
+
+desktopctl.local_ip () {
+ case "$1" in
+ '') desktopctl.local_ip.get "$@" ;;
+ *) desktopctl.local_ip.usage "$@" ;;
+ esac
+}
+# }}}}}
+# Media watch time {{{{{
+desktopctl.mediawatchtime.usage () {
+cat <<EOF
+[desktopctl] Media Watch Time
+
+Prints watch times of media with remaining time and percentage.
+
+Requires 'column' from 'util-linux'.
+
+Arguments:
+ A list of media files (mp4)
+
+Example:
+
+$ cd tv-shows/planet-earth
+$ desktopctl mediawatchtime *mp4
+Item | Title | Duration | Watched | % Watched | Remaining
+1 | S01E01-planet-earth-from-pole-to-pole.mp4: | 0:59 | 0:59 | 9.00 | 9:44
+2 | S01E02-planet-earth-mountains.mp4: | 0:57 | 1:56 | 18.00 | 8:47
+3 | S01E03-planet-earth-fresh-water.mp4: | 0:58 | 2:55 | 27.00 | 7:48
+4 | S01E04-planet-earth-caves.mp4: | 0:57 | 3:52 | 36.00 | 6:51
+5 | S01E05-planet-earth-deserts.mp4: | 0:57 | 4:50 | 45.00 | 5:53
+6 | S01E06-planet-earth-ice-worlds.mp4: | 0:58 | 5:49 | 54.00 | 4:54
+7 | S01E07-planet-earth-great-plains.mp4: | 0:58 | 6:48 | 63.00 | 3:55
+8 | S01E08-planet-earth-jungles.mp4: | 0:58 | 7:46 | 72.00 | 2:56
+9 | S01E09-planet-earth-shallow-seas.mp4: | 0:58 | 8:45 | 81.00 | 1:57
+10 | S01E10-planet-earth-seasonal-forests.mp4: | 0:58 | 9:44 | 90.00 | 0:58
+11 | S01E11-planet-earth-ocean-deep.mp4: | 0:58 | 10:43 | 100.00 | 0:00
+EOF
+}
+
+desktopctl.mediawatchtime.list () {
+ TMP=$(mktemp /tmp/lengths.XXX)
+
+ to_hrs () {
+ awk '{a=$1; printf "%s:%02d", int(a) , int((a - int(a))*60)""}'
+ }
+
+ to_dec () {
+ awk -F: '{print $1+$2/60+$3/3600}'
+ }
+
+ bcr () {
+ bc -l <<< "scale = 2; $*"
+ }
+
+ for vid in "$@"
+ do
+ echo -n "$vid: "
+ ffmpeg -hide_banner -i "$vid" 2>&1 | grep -o 'Duration: [^,]*'
+ done > "$TMP"
+
+ TOTAL_HOURS=$(awk '{print $3}' "$TMP" | to_dec | paste -sd+ | bc -l)
+ watched=0
+ n=0
+
+ while read -r title _ duration
+ do
+ duration=$(to_dec <<< "$duration")
+ watched=$(bcr "$watched + $duration")
+ remaining=$(bcr "$TOTAL_HOURS - $watched")
+ percentage=$(bcr "$watched/$TOTAL_HOURS*100")
+
+ duration=$(to_hrs <<< "$duration")
+ remaining=$(to_hrs <<< "$remaining")
+ ((n++))
+
+ echo "$n $title $duration $(to_hrs <<< "$watched") $percentage $remaining"
+ done < "$TMP" | column -t -o" | " -N "Item, Title, Duration, Watched, % Watched, Remaining"
+
+ rm "$TMP"
+}
+
+desktopctl.mediawatchtime () {
+ case "$1" in
+ ''|-h|--help|help) desktopctl.mediawatchtime.usage "$@" ;;
+ *) desktopctl.mediawatchtime.list "$@" ;;
+ esac
+}
+# }}}}}
+# Memory {{{{{
+desktopctl.memory.usage () {
+cat <<EOF
+[desktopctl] Memory
+
+Print memory usage (human readable).
+
+Options
+ -r, --raw, raw, Display in raw bytes (not human readable).
+EOF
+}
+
+desktopctl.memory.get () {
+ # Get amount of memory being used by the computer as reported by
+ # /proc/meminfo
+ #
+ # Arguments: None
+
+ awk '{ if($0 ~ "^(MemTotal|Shmem):") {total+=$2}
+ else if($0 ~ "^(MemFree|Buffers|Cached|SReclaimable):") {total-=$2}
+ } END {print total}' /proc/meminfo
+}
+
+desktopctl.memory () {
+ case "$1" in
+ -r|--raw|raw) desktopctl.memory.get ;;
+ '') numfmt --from-unit=1024 --to=iec <<< "$(desktopctl.memory.get)" ;;
+ *) desktopctl.memory.usage ;;
+esac
+}
+# }}}}}
+# Monitor directory {{{{{
+desktopctl.mondir.usage () {
+cat <<EOF
+[desktopctl] Monitor directory for file changes
+
+Prints size, number of files in directory, and changes since last polling.
+
+Arguments:
+ dir, Directory or file to monitor
+ -i, --interval, Time interval to re-compute file sizes and changes in seconds
+ (Default 60)
+
+Useful for independently monitoring file transfer progress.
+
+E.g.
+
+$ rsync /path/to/src/dir /path/to/dest/dir
+
+Forgot to add progress options! ... instead of cancelling the transfer, just
+monitor the destination directory directly
+
+$ desktopctl mondir -i 5 /path/to/dest/dir
+mondir: Monitoring '/path/to/dest/dir' with interval 5s.
+
+2024-03-30-15:23:14 Total: 188G Files: 349222
+2024-03-30-15:23:20 Total: 188G Files: 349266 Diff: 500MiB Files diff: 44 Speed: 100MiB/s
+2024-03-30-15:23:26 Total: 189G Files: 349339 Diff: 525MiB Files diff: 73 Speed: 105MiB/s
+...
+
+Of course this is also useful for tools that do not offer progress statistics.
+EOF
+}
+
+desktopctl.mondir.monitor () {
+ if [[ $1 == -i ]] || [[ $1 == --interval ]]
+ then
+ if [[ $2 =~ ^[0-9]+$ ]]
+ then
+ INTERVAL=$2
+ shift
+ shift
+ else
+ echo "mondir: Error: Interval '$2' must be a non-negative integer."
+ exit 1
+ fi
+ else
+ INTERVAL=60
+ fi
+
+ if [[ ! -r $1 ]]
+ then
+ echo "mondir: Please enter a valid file/directory."
+ exit 1
+ fi
+
+ MONITOR=$1
+
+ echo "mondir: Monitoring '$MONITOR' with interval ${INTERVAL}s".
+ echo
+
+ size_pre=0
+ files_pre=0
+
+ while true
+ do
+ size=$(du -s "$MONITOR" | cut -f 1)
+ files=$(find "$MONITOR" -type f | wc -l)
+ time=$(date +"%F-%H:%M:%S")
+
+ sizediff=$((size - size_pre))
+ filediff=$((files - files_pre))
+
+ baudrate=$((sizediff / INTERVAL))
+
+ size_human=$(numfmt --from-unit=1000 --to=si <<< "$size")
+ sizediff_human=$(numfmt --from-unit=1000 --to=si <<< "$sizediff")
+ baudrate_human=$(numfmt --from-unit=1000 --to=si <<< "$baudrate")
+
+ if ((size_pre == 0)) && ((files_pre == 0))
+ then
+ printf "%s\tTotal: %s\tFiles: %s\n" "$time" "$size_human" "$files"
+ else
+ printf "%s\tTotal: %s\tFiles: %s\tDiff: %s\tFiles diff:%s\tSpeed: %s/s\n" \
+ "$time" \
+ "$size_human" \
+ "$files" \
+ "$sizediff_human" \
+ "$filediff" \
+ "$baudrate_human"
+ fi
+
+ size_pre=$size
+ files_pre=$files
+
+ sleep "$INTERVAL"
+ done
+}
+
+desktopctl.mondir () {
+ case "$1" in
+ '') desktopctl.mondir.usage "$@" ;;
+ *) desktopctl.mondir.monitor "$@" ;;
+ esac
+}
+
+# }}}}}
+# Online {{{{{
+desktopctl.online.usage () {
+cat <<EOF
+[desktopctl] Check internet connection
+
+Options:
+ -t, --timeout, Set timeout in seconds
+ -q, --quiet, Do not print. Only uses exit value
+
+Note: Options must be specified separately i.e. '-q -t 30' and not '-qt30' or
+'-qt 30'.
+
+Useful in scripts, to start backups or synchronise emails only when connected to
+the internet.
+EOF
+}
+
+desktopctl.online.check () {
+ time=1
+ quiet=false
+
+ while true
+ do
+ case $1 in
+ -q|--quiet)
+ shift
+ quiet=true
+ ;;
+ -t|--timeout)
+ shift
+ case $1 in
+ +([0-9]))
+ time=$1
+ ;;
+ '')
+ echo "desktopctl.online: Must specify time value"
+ return 1
+ ;;
+ *)
+ echo "desktopctl.online: $1 not a time value"
+ return 1
+ esac ;;
+ *) break ;;
+ esac
+ done
+
+ while ((time > -1))
+ do
+ # https://unix.stackexchange.com/a/190610
+ if nc -zw1 google.com 443 2>/dev/null \
+ && awk '$1 " " $2 == "SSL handshake" {getline; exit $2 != "OK"}' \
+ <(echo | openssl s_client -connect "google.com:443" 2>&1)
+ then
+ [[ $quiet == false ]] && echo "desktopctl.online: Connection was succesful!"
+ return 0
+ fi
+ if (( time > 0 ))
+ then
+ sleep 1
+ fi
+ time=$((time-1))
+ done
+ [[ $quiet == false ]] && echo "desktopctl.online: Could not connect to the internet."
+ return 1
+}
+
+desktopctl.online () {
+ case "$1" in
+ ''|-q|--quiet|-t|--timeout) desktopctl.online.check "$@" ;;
+ *) desktopctl.online.usage "$@" ;;
+ esac
+}
+# }}}}}
+# Power Management (Dell) {{{{{
+desktopctl.power.usage () {
+cat <<EOF
+[desktopctl] Power Management Options (Dell Only)
+
+Charging Profiles:
+ ac, Primarily AC Charging, Auto performance
+ express, Express charging, Low performance
+
+Performance:
+ auto, Automatically adjust performance
+ cool, Set performance profile to cool
+ desktop, Highest performance settings
+ laptop, Laptop performance settings
+ low, Set perforamnce to power saving
+ quiet, Set performance profile to quiet
+EOF
+}
+
+desktopctl.power.cool () {
+ echo "desktopctl: Setting dell thermal profile to cool..."
+ sudo cctk --ThermalManagement=Cool > /dev/null 2>&1
+}
+desktopctl.power.quiet () {
+ echo "desktopctl: Setting dell thermal profile to quiet..."
+ sudo cctk --ThermalManagement=Quiet > /dev/null 2>&1
+}
+desktopctl.power.ultraperformance () {
+ echo "desktopctl: Setting dell thermal profile to UltraPerformance..."
+ sudo cctk --ThermalManagement=UltraPerformance > /dev/null 2>&1
+}
+desktopctl.power.express () {
+ echo "desktopctl: Setting dell charging profile to express..."
+ sudo cctk --PrimaryBattChargeCfg=Express > /dev/null 2>&1
+}
+desktopctl.power.ac () {
+ echo "desktopctl: Setting dell charging profile to AC..."
+ sudo cctk --PrimaryBattChargeCfg=AC > /dev/null 2>&1
+}
+
+desktopctl.tlp.start () {
+ echo "desktopctl: Starting TLP in automatic mode..."
+ sudo tlp start > /dev/null 2>&1
+}
+desktopctl.tlp.ac () {
+ echo "desktopctl: Starting TLP in AC mode..."
+ sudo tlp ac > /dev/null 2>&1
+}
+desktopctl.tlp.bat () {
+ echo "desktopctl: Starting TLP in bat mode..."
+ sudo tlp bat > /dev/null 2>&1
+}
+
+desktopctl.governor.powersave () {
+ echo "desktopctl: Setting CPU governor to powersave..."
+ sudo cpupower frequency-set --governor powersave > /dev/null 2>&1
+}
+desktopctl.governor.performance () {
+ echo "desktopctl: Setting CPU governor to performance..."
+ sudo cpupower frequency-set --governor performance > /dev/null 2>&1
+}
+
+desktopctl.frequency.set () {
+ hr=$(bc -l <<< "scale=1;$1/1000000")GHz
+ echo "desktopctl: Setting CPU frequency to $hr..."
+ sudo cpupower frequency-set --max "$1" > /dev/null 2>&1
+}
+
+desktopctl.power.desktop () {
+ desktopctl.tlp.ac
+ desktopctl.power.ultraperformance
+ desktopctl.governor.performance
+ desktopctl.frequency.set 4800000
+}
+
+desktopctl.power.laptop () {
+ desktopctl.tlp.bat
+ desktopctl.power.quiet
+ desktopctl.governor.powersave
+ desktopctl.frequency.set 2000000
+}
+
+desktopctl.power.low_power () {
+ desktopctl.tlp.bat
+ desktopctl.power.quiet
+ desktopctl.governor.powersave
+ desktopctl.frequency.set 1000000
+}
+
+desktopctl.power.auto () {
+ desktopctl.tlp.start
+ if [[ $(cat /sys/class/power_supply/AC/online) == 1 ]]
+ then
+ dunstify --replace=2393 --urgency=low "Being charged"
+ desktopctl.power.ultraperformance
+ desktopctl.governor.performance
+ desktopctl.frequency.set 4800000
+ else
+ dunstify --replace=2393 --urgency=low "On battery"
+ desktopctl.power.quiet
+ desktopctl.governor.powersave
+ desktopctl.frequency.set 2000000
+ fi
+}
+
+desktopctl.power () {
+ while true
+ do
+ case $1
+ in
+ ac) shift; desktopctl.power.ac "$@" ;;
+ auto) shift; desktopctl.power.auto "$@" ;;
+ cool) shift; desktopctl.power.cool "$@" ;;
+ desktop) shift; desktopctl.power.desktop "$@" ;;
+ express) shift; desktopctl.power.low_power; desktopctl.power.express "$@" ;;
+ laptop) shift; desktopctl.power.laptop "$@" ;;
+ low|low-power) shift; desktopctl.power.low_power "$@" ;;
+ quiet) shift; desktopctl.power.quiet "$@" ;;
+ '') break ;;
+ *) shift; desktopctl.power.usage "$@" ; break ;;
+ esac
+ done
+}
+# }}}}}
+# Reboot required (Archlinux Only) {{{{{
+desktopctl.reboot-required.usage () {
+cat <<EOF
+[desktopctl] Reboot Required? (Archlinux Only)
+
+Prints information about what services need to be restarted, and whether the
+kernel is old
+
+Used to generally inform, whether the system should be rebooted
+
+Can be wrapped with pacman hook to give information after update
+
+Arguments: None
+
+Options:
+ -d, --detailed, Print more detailed information
+EOF
+}
+
+desktopctl.reboot-required.check () {
+ running_kernel=$(uname -r)
+ if grep -qF -- '-lts' <<< "$running_kernel"
+ then
+ installed_kernel=$(file "$(pacman -Qql linux-lts | grep vmlinuz)" | awk '{for (i=1;i<NF;i++) if ($i == "version") print $(i+1)}')
+ else
+ installed_kernel=$(file "$(pacman -Qql linux | grep vmlinuz)" | awk '{for (i=1;i<NF;i++) if ($i == "version") print $(i+1)}')
+ fi
+
+ if [[ "$installed_kernel" != "$running_kernel" ]]
+ then
+ echo
+ echo -e "\e[1m:: Kernel out of date\e[0m"
+ echo
+ echo " Currently installed: $installed_kernel > Running kernel: $running_kernel"
+ echo
+ else
+ echo "Running kernel is up to date"
+ fi
+
+ if [[ $1 == --d ]] || [[ $1 == --detailed ]]
+ then
+ declare -A affected_executables
+ while read -r executable vlibrary
+ do
+ affected_executables[$vlibrary]+=" $executable"
+ done < <(sudo lsof -n +c 0 2>/dev/null | awk '/DEL.*\/usr\/lib/ {print $1 " " $NF}' | sort -u)
+
+ if (( ${#affected_executables[@]} > 0 ))
+ then
+ echo -e "\e[1m:: Out of date libraries\e[0m\n"
+ {
+ echo "Library| Affected executables"
+ echo "-------| --------------------"
+ for vlibrary in "${!affected_executables[@]}"
+ do
+ library=$(basename "$vlibrary" | sed 's/.so.*$/.so/')
+ executables=${affected_executables[$vlibrary]}
+ executables=${executables## }
+ echo "$library| $executables"
+ done | sort -k2 -t\|
+ } | column -c 80 -Lto ' | ' -s \| -c 80 | sed 's/^/ /'
+ echo
+ else
+ echo "No libraries out of date"
+ fi
+ else
+ executables=$(sudo lsof -n +c 0 2>/dev/null | awk '/DEL.*\/usr\/lib/ {print $1}' | sort -u)
+ if [[ -n $executables ]]
+ then
+ echo -e "\e[1m:: Out of date programs\e[0m\n"
+ sed 's/^/ /' <<< "$executables"
+ else
+ echo "No programs out of date"
+ fi
+ fi
+ echo -n "Uptime: "
+ uptime -p
+}
+
+desktopctl.reboot-required () {
+ case "$1" in
+ ''|-d|--detailed) desktopctl.reboot-required.check "$@" ;;
+ *) desktopctl.reboot-required.usage "$@" ;;
+ esac
+}
+# }}}}}
+# Screenshot {{{{{
+desktopctl.screenshot.usage () {
+cat <<EOF
+[desktopctl] Take screenshots
+
+Supports taking a partial selection of the screen
+
+Supports wayland/x11
+
+Screenshots are copied to the clipboard
+
+Arguments:
+ --select, Will ask user to make selection of area to screenshot
+ --blurred, (x11 only) Will blur screenshot
+
+Blurred screenshots are nice for creating a lockscreen with current screen
+contents blurred.
+EOF
+}
+
+desktopctl.screenshot.screenshot () {
+ set -e
+ mkdir -p "$SCREENSHOTS"
+ SCREENSHOT="$SCREENSHOTS/screenshot-$(date +"%Y%m%d-%H%M%S").png"
+
+ if [[ $WINDOWING == wayland ]]
+ then
+ if [[ $1 == --select ]]
+ then
+ grim -g "$(slurp)" "$SCREENSHOT"
+ else
+ grim "$SCREENSHOT"
+ fi
+ wl-copy < "$SCREENSHOT"
+ else
+ if [[ $1 == --blurred ]]
+ then
+ if [[ $2 == "" ]]
+ then
+ SCREENSHOT="$HOME/.cache/blurred-screenshot.png"
+ else
+ SCREENSHOT="$2"
+ fi
+ # Ffmpeg is (surprisingly?) much faster at creating a screenshot and
+ # blurring than other tools
+ # Speed is important if this is being used to create a live blurred
+ # screenshot as the lockscreen (you don't want locking to take 30 seconds)
+ #
+ # Not sure how to get video_size in a platform independent way
+ ffmpeg -loglevel quiet -f x11grab -video_size "$SCREENSHOT_SIZE" -y -i "$DISPLAY" -filter_complex "boxblur=10:1" -vframes 1 "$SCREENSHOT"
+ exit
+ fi
+
+ if [[ $1 == --select ]]
+ then
+ setxkbmap -option grab:break_actions
+ xdotool key XF86Ungrab
+ xdotool mousemove_relative polar 90
+ import "$SCREENSHOT"
+ else
+ import -window root "$SCREENSHOT"
+ fi
+ xclip -selection clipboard "$SCREENSHOT"
+ fi
+
+ ln -nfs "$SCREENSHOT" "$SCREENSHOTS/screenshot-latest.png"
+
+ SCREENSHOT=$(basename "$SCREENSHOT")
+ notify-send "Screenshot: $SCREENSHOT"
+}
+
+desktopctl.screenshot () {
+ case "$1" in
+ ""|--select|--blurred) desktopctl.screenshot.screenshot "$@" ;;
+ *) desktopctl.screenshot.usage "$@" ;;
+ esac
+}
+# }}}}}
+# Sound {{{{{
+desktopctl.sound.usage () {
+cat <<EOF
+[desktopctl] Sound management
+
+Arguments:
+ +, Increase volume
+ -, Decrease volume
+ sink, Print current default output sink
+ ismuted, Print "yes" if muted all devices are muted, else no
+ notify, Send a desktop notification of current volume
+ get-volume, get Print current volume
+ mute, Mute current default sink
+ toggle, Toggle mute status
+
+Uses pactl under the hood
+
+Useful for displaying current volume in status line or when binding keyboard
+buttons.
+EOF
+}
+
+desktopctl.sound.mute() {
+ pactl set-sink-mute @DEFAULT_SINK@ true
+}
+
+desktopctl.sound.unmute() {
+ pactl set-sink-mute @DEFAULT_SINK@ false
+}
+
+desktopctl.sound.togglemute() {
+ pactl set-sink-mute @DEFAULT_SINK@ toggle
+}
+
+desktopctl.sound.ismuted () {
+ if grep -q no <(pactl list sinks | awk -F'\\s*:\\s*' '/Mute/ {print $2}')
+ then
+ return 1
+ else
+ return 0
+ fi
+}
+
+desktopctl.sound.volume.notify () {
+ if command -v dunstify > /dev/null
+ then
+ dunstify --replace=1 --urgency=low "$(desktopctl.sound.volume)"
+ else
+ notify-send "$(desktopctl.sound.volume)"
+ fi
+}
+
+desktopctl.sound._volume () {
+ pactl --format json list | jq -r ".sinks[] | select(.name == \"$(pactl get-default-sink)\") | .volume[] | .value_percent" | head -1
+}
+
+desktopctl.sound._equalise_volume () {
+ pactl set-sink-volume @DEFAULT_SINK@ "$(desktopctl.sound._volume)"
+}
+
+desktopctl.sound.volume () {
+ if [[ $1 == --status ]]
+ then
+ if ! desktopctl.sound.ismuted
+ then
+ desktopctl.sound._volume
+ fi
+ else
+ if desktopctl.sound.ismuted
+ then
+ echo "Muted"
+ else
+ echo "Volume: $(desktopctl.sound._volume)"
+ fi
+ fi
+}
+
+desktopctl.sound.sink () {
+ ALSA_DEFAULT_SINK=$(pactl get-default-sink)
+ DEFAULT_SINK=$(pactl list sinks | awk -v DefaultSink="$ALSA_DEFAULT_SINK" '$0 ~ DefaultSink {getline; print}' | awk -F'\\s*:\\s*' '/Description/ {print $2}')
+
+ BT_ID=$(sed 's/.*bluez_output.\(..\)_\(..\)_\(..\)_\(..\)_\(..\)_\(..\)\..*/\1:\2:\3:\4:\5:\6/' <<< "$ALSA_DEFAULT_SINK")
+
+ if ! [[ "$BT_ID" == "" ]] && [[ $(systemctl is-active bluetooth) == active ]]
+ then
+ DEFAULT_SINK="$DEFAULT_SINK ($(bluetoothctl info "$BT_ID" | awk -F: '/Battery Percentage/ {print $2}' | sed 's/.*(\(.*\))$/\1/')%)"
+ fi
+
+ # Requires pulsemixer dependency
+ # DEFAULT_SINK=$(pulsemixer --list-sinks | sed -n 's/.*Name: \([^, ]*\).*Default/\1/p')
+
+ if [[ $DEFAULT_SINK =~ (Built-in *|Tiger Lake-LP) ]]
+ then
+ # Need to print some output
+ echo
+ else
+ echo "$DEFAULT_SINK"
+ fi
+}
+
+desktopctl.sound.daemon () {
+ # Automatically mute sound if nothing is playing for a while
+
+ if (($(pgrep -u "$UID" -cxf '^.*/de sound --daemon.*') > 1))
+ then
+ desktopctl.log "sound: Daemon already running."
+ exit 1
+ fi
+
+ NOT_CONNECTED=0
+ THRESHOLD=10
+ NOTIFIED=false
+
+ while true
+ do
+ if ! desktopctl.sound.ismuted && (($(grep -c 'Corked: no' < <(pactl list sink-inputs)) == 0))
+ then
+ ((NOT_CONNECTED++))
+
+ if ((NOT_CONNECTED > "$THRESHOLD"))
+ then
+ desktopctl.log "sound: Muting sound"
+ NOT_CONNECTED=0
+ desktopctl.sound mute
+ fi
+ NOTIFIED=false
+ else
+ NOT_CONNECTED=0
+ NOTIFIED=true
+ fi
+
+ sleep 60
+ done &
+ exit
+}
+
+desktopctl.sound () {
+ case $1 in
+ "+")
+ if ! desktopctl.sound.ismuted || [[ $(desktopctl.sound.volume) = "Volume: 0%" ]]
+ then
+ if (($(desktopctl.sound._volume | sed 's/%//') > 90))
+ then
+ pactl set-sink-volume @DEFAULT_SINK@ 100%
+ else
+ pactl set-sink-volume @DEFAULT_SINK@ +5%
+ fi
+ fi
+
+ desktopctl.sound.unmute
+ desktopctl.sound.volume.notify
+ ;;
+ "-")
+ if [[ $(desktopctl.sound.volume) = "Volume: 5%" ]]
+ then
+ desktopctl.sound.mute
+ fi
+
+ pactl set-sink-volume @DEFAULT_SINK@ -5%
+ desktopctl.sound.volume.notify
+ ;;
+ "toggle")
+ pactl set-sink-mute @DEFAULT_SINK@ toggle
+ desktopctl.sound.volume.notify
+ ;;
+ "mute")
+ pactl set-sink-mute @DEFAULT_SINK@ true
+ desktopctl.sound.volume.notify
+ ;;
+ "volume"|"get-volume"|"get")
+ shift
+ desktopctl.sound.volume "$@"
+ ;;
+ "sink")
+ desktopctl.sound.sink
+ ;;
+ "ismuted")
+ if desktopctl.sound.ismuted
+ then
+ echo "True"
+ else
+ echo "False"
+ exit 2
+ fi
+ ;;
+ "playing")
+ if grep -q playing$ <(playerctl metadata -af '{{lc(status)}}' 2>/dev/null)
+ then
+ echo ""
+ elif grep -q paused$ <(playerctl metadata -af '{{lc(status)}}' 2>/dev/null)
+ then
+ echo ""
+ fi
+ ;;
+ "--daemon")
+ shift
+ desktopctl.sound.daemon
+ ;;
+ "notify")
+ desktopctl.sound.volume.notify
+ ;;
+ *)
+ desktopctl.sound.usage
+ exit 1
+ ;;
+ esac
+}
+# }}}}}
+# Speedtest {{{{{
+desktopctl.speedtest.usage () {
+cat <<EOF
+[desktopctl] Internet speedtest
+
+Very crude. Uses curl to download the file
+
+ http://eu-central-1.linodeobjects.com/speedtest/100MB-speedtest
+
+and uses pipeview to monitor the speed.
+
+Arguments: None
+EOF
+}
+
+desktopctl.speedtest.test () {
+ # --bits is a relatively new option for pipeview (pv)
+ if head /dev/null | pv --bits --format '' > /dev/null 2>&1
+ then
+ curl --silent http://eu-central-1.linodeobjects.com/speedtest/100MB-speedtest \
+ | pv --bits --wait --format 'Curr: %r Avg: %a' > /dev/null
+ else
+ curl --silent http://eu-central-1.linodeobjects.com/speedtest/100MB-speedtest \
+ | pv --wait --format 'Curr: %r Avg: %a' > /dev/null
+ fi
+}
+
+desktopctl.speedtest () {
+ case "$1" in
+ '') desktopctl.speedtest.test "$@" ;;
+ *) desktopctl.speedtest.usage "$@" ;;
+ esac
+}
+# }}}}}
+# SSH devices {{{{{
+desktopctl.ssh_devices.usage () {
+cat <<EOF
+[desktopctl] Print list of SSH devices on network
+
+Useful for finding headless machine on network after setup (e.g. Raspberry Pi)
+
+Pretty prints table with device names
+
+Arguments: None
+EOF
+}
+
+desktopctl.ssh_devices.find () {
+ local_ip=$(desktopctl.local_ip | awk -F. '{printf "%s.%s.%s.*\n", $1, $2, $3}')
+ echo "Scanning port 22 on local subnet '$local_ip'"
+ echo
+ nmap -oG - -p 22 "$local_ip" | grep -v '^#' | paste - - | awk '{ gsub("\\(|\\)", "", $3) ; gsub(".home$", "", $3) ; sub("^$", "*NO_HOSTNAME*", $3) ; print $2, $3}' | column -t -o" | " --table-columns="Local IP,Hostname"
+}
+
+desktopctl.ssh_devices () {
+ case "$1" in
+ '') desktopctl.ssh_devices.find "$@" ;;
+ *) desktopctl.ssh_devices.usage "$@" ;;
+ esac
+}
+# }}}}}
+# Timezone {{{{{
+desktopctl.timezone.usage () {
+cat <<EOF
+[desktopctl] Get current timezone based on IP
+
+Uses http://ip-api.com
+
+Arguments: None
+Options:
+ update, Update timezone to current tz using timedatectl
+EOF
+}
+
+desktopctl.timezone.get () {
+ curl -SsL "http://ip-api.com/line/?fields=timezone" 2>/dev/null
+}
+
+desktopctl.timezone.update () {
+ tz=$(desktopctl.timezone)
+ if [[ -n $tz ]]
+ then
+ timedatectl set-timezone "$tz"
+ fi
+}
+
+desktopctl.timezone () {
+ case "$1" in
+ '') desktopctl.timezone.get "$@" ;;
+ update) shift; desktopctl.timezone.update "$@" ;;
+ *) desktopctl.timezone.usage "$@" ;;
+ esac
+}
+# }}}}}
+# Wifi {{{{{
+desktopctl.wifi.usage () {
+cat <<EOF
+[desktopctl] Wifi management
+
+Arguments:
+ ssid, Print connected ssid
+ icons, Print ssid with icons
+ only-icons Only print icons
+ off, Turn wifi off
+ on, Turn wifi on
+EOF
+}
+
+desktopctl.wifi.info () {
+ case $WIFI_BACKEND in
+ iwd)
+ _info=$(iwctl station wlan0 show | sed '1,5d;s/\x1B\[[0-9;]\{1,\}[A-Za-z]//g;s/^\s*//;/^\s*$/d' | awk -F'\\s+\\s+' '$1 == "State" {print $2}')
+ case "$_info" in
+ "") info=off;;
+ "disconnected") info=disconnected;;
+ "configuring") info=configuring;;
+ "connected") info=connected;;
+ esac
+
+ echo "$info"
+ ;;
+ nm)
+ _info=$(nmcli -g general.state device show "$WIFI_DEVICE" | grep -v '^$' | awk '{print $1}')
+ case "$_info" in
+ "20") info=off;;
+ "30") info=disconnected;;
+ "50") info=configuring;;
+ "70") info=configuring;;
+ "100") info=connected;;
+ esac
+
+ echo "$info"
+ ;;
+ esac
+}
+
+desktopctl.wifi.ssid () {
+ iwgetid -r
+
+ # Deprecated version
+ # case $WIFI_BACKEND in
+ # iwd)
+ # iwctl station wlan0 show | sed '1,5d;s/\x1B\[[0-9;]\{1,\}[A-Za-z]//g;s/^\s*//;/^\s*$/d' \
+ # | awk -F'\\s+\\s+' '$1 == "Connected network" {print $2}'
+ # ;;
+ # nm)
+ # nmcli -g general.connection device show "$WIFI_DEVICE"
+ # ;;
+ # esac
+}
+
+desktopctl.wifi.strength () {
+ case $WIFI_BACKEND in
+ iwd)
+ iwctl station wlan0 show \
+ | sed '1,5d;s/\x1B\[[0-9;]\{1,\}[A-Za-z]//g;s/^\s*//;/^\s*$/d' \
+ | awk -F'\\s+\\s+' '$1 == "RSSI" {print $2}'\
+ | awk '{a=-$1;a=a<40?40:a;a=a>100?100:a;print int(100-(a-40)/60*100)}'
+ ;;
+ nm)
+ nmcli -t -f IN-USE,SIGNAL device wifi | awk -F: '/*/ {print $2}' | head -1
+ ;;
+ generic)
+ # Link quality is apparently maximal at 70
+ awk '$1 ~ /wlan0/ {gsub("\\.", "", $3); printf "%.0f\n", $3/70*100}' /proc/net/wireless
+ ;;
+ esac
+}
+
+desktopctl.wifi () {
+ info=$(desktopctl.wifi.info)
+ ssid=$(desktopctl.wifi.ssid)
+
+ _ssid=$ssid
+
+ for KNOWN_NETWORK in "${KNOWN_NETWORKS[@]}"
+ do
+ echo -n
+ if [[ "$KNOWN_NETWORK" == "$ssid" ]]
+ then
+ _ssid=""
+ fi
+ done
+
+ strength=$(desktopctl.wifi.strength)
+
+ if [[ "$strength" == "" ]]
+ then
+ WIFI_ICON_ON=$WIFI_ICON_DISCONNECTED
+ elif ((strength < 26))
+ then
+ WIFI_ICON_ON=$WIFI_ICON_LOW_STRENGTH
+ elif ((strength < 50))
+ then
+ WIFI_ICON_ON=$WIFI_ICON_MED_STRENGTH
+ else
+ WIFI_ICON_ON=$WIFI_ICON_HIG_STRENGTH
+ fi
+
+ case "$1" in
+ ssid)
+ echo "$ssid"
+ ;;
+ icons)
+ case "$info" in
+ off) echo "$WIFI_ICON_OFF" ;;
+ disconnected) echo "$WIFI_ICON_DISCONNECTED" ;;
+ configuring) echo "$WIFI_ICON_CONFIGURING $ssid" ;;
+ connected) echo "$WIFI_ICON_ON $ssid" ;;
+ esac
+ ;;
+ only-icons)
+ case "$info" in
+ off) echo "$WIFI_ICON_OFF" ;;
+ disconnected) echo "$WIFI_ICON_DISCONNECTED" ;;
+ configuring) echo "$ssid $WIFI_ICON_CONFIGURING" ;;
+ connected) echo "$_ssid $WIFI_ICON_ON" ;;
+ esac
+ ;;
+ off)
+ wifi off ;;
+ on)
+ wifi on ;;
+ -h|--help|help)
+ desktopctl.wifi.usage "$@" ;;
+ *)
+ case "$info" in
+ off) echo "Off" ;;
+ disconnected) echo "Disconnected" ;;
+ configuring) echo "Configuring: $ssid" ;;
+ connected) echo "Connected: $ssid" ;;
+ esac
+ ;;
+ esac
+}
+# }}}}}
+# Xwayland windows (sway only) {{{{{
+desktopctl.xwayland.usage () {
+cat <<EOF
+[desktopctl] Xwayland windows (Sway only)
+
+Count xwayland clients
+
+Arguments:
+ format, A format string in which '%n' will replace number of Xwayland clients
+ if there are any. If there are none, nothing will be printed.
+EOF
+}
+
+desktopctl.xwayland.count () {
+ xwayland_clients=0
+ verbose=false
+
+ if command -v swaymsg > /dev/null
+ then
+ xwayland_clients=$(swaymsg -t get_tree | grep -c xwayland)
+ else
+ echo "swaymsg not running..."
+ exit 2
+ fi
+
+ if [[ $1 == -v ]]
+ then
+ shift
+ verbose=true
+ fi
+
+ if [[ -z $1 ]]
+ then
+ fmt="Active xwayland clients: %n"
+ else
+ fmt=$1
+ fi
+
+ if ((xwayland_clients > 0)) || [[ $verbose == true ]]
+ then
+ echo "${fmt/\%n/$xwayland_clients}"
+ fi
+}
+
+desktopctl.xwayland () {
+ case "$1" in
+ -h|--help|help) desktopctl.xwayland.usage "$@" ;;
+ *) desktopctl.xwayland.count "$@" ;;
+ esac
+}
+# }}}}}
+
+# Help {{{{{
+desktopctl.help () {
+ echo "desktopctl: available commands"
+ echo
+ {
+ echo "auth - Show session authentication status"
+ echo "backlight - Get and set backlight"
+ echo "battery - Get battery status (Percentage, Current consumption, etc)"
+ echo "bt|bluetooth - Get and set Bluetooth"
+ echo "color|colour - Colour picker"
+ echo "config_diff - Show configuration file differences (Archlinux only)"
+ echo "cpu - Get CPU frequency"
+ echo "headphones - Get whether headpones are plugged in"
+ echo "ip - Get local IP address"
+ echo "mediawatchtime - Print total media time of listed files"
+ echo "memory - Get memory usage"
+ echo "mondir - Monitor directories/files"
+ echo "online - Get current internet connection status"
+ echo "power - Set power and charging profiles (Dell only)"
+ echo "reboot-required - List programs that need to be restarted (Archlinux only)"
+ echo "screenshot - Take screenshots"
+ echo "sound - Get and set sound"
+ echo "speedtest - Show current internet speed"
+ echo "ssh - List ssh clients on local network"
+ echo "timezone|gettz - Get current timezone"
+ echo "wifi - Set wifi on/off"
+ echo "xwayland - List number of xwayland clients (sway only)"
+ } | column -xc80
+ echo
+ echo "Configuration file is at ~/.config/desktopctl/desktopctlrc"
+}
+# }}}}}
+# Longhelp {{{{{
+desktopctl.longhelp () {
+ {
+ desktopctl.help
+ echo -e '\n================================================================================'
+ echo "Individual help pages"
+ echo -e '================================================================================\n'
+
+ echo -e '--------------------------------------------------------------------------------\n'
+ desktopctl.session_auth.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.backlight.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.battery.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.bt.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.cpu.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.colour_picker.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.config_diff.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.headphones.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.online.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.local_ip.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.mediawatchtime.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.memory.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.mondir.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.power.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.reboot-required.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.screenshot.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.sound.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.speedtest.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.ssh_devices.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.timezone.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.wifi.usage
+ echo -e '\n--------------------------------------------------------------------------------\n'
+ desktopctl.xwayland.usage
+
+ } | less
+}
+# }}}}}
+
+case $1 in
+ auth) shift; desktopctl.session_auth "$@" ;;
+ backlight) shift; desktopctl.backlight "$@" ;;
+ battery) shift; desktopctl.battery "$@" ;;
+ bt|bluetooth) shift; desktopctl.bt "$@" ;;
+ color|colour) shift; desktopctl.colour_picker "$@" ;;
+ config_diff) shift; desktopctl.config_diff "$@" ;;
+ cpu) shift; desktopctl.cpu "$@" ;;
+ headphones) shift; desktopctl.headphones "$@" ;;
+ ip) shift; desktopctl.local_ip "$@" ;;
+ mediawatchtime) shift; desktopctl.mediawatchtime "$@" ;;
+ memory) shift; desktopctl.memory "$@" ;;
+ mondir) shift; desktopctl.mondir "$@" ;;
+ online) shift; desktopctl.online "$@" ;;
+ power) shift; desktopctl.power "$@" ;;
+ reboot-required) shift; desktopctl.reboot-required "$@" ;;
+ screenshot) shift; desktopctl.screenshot "$@" ;;
+ sound) shift; desktopctl.sound "$@" ;;
+ speedtest) shift; desktopctl.speedtest "$@" ;;
+ ssh) shift; desktopctl.ssh_devices "$@" ;;
+ timezone|gettz) shift; desktopctl.timezone "$@" ;;
+ wifi) shift; desktopctl.wifi "$@" ;;
+ xwayland) shift; desktopctl.xwayland "$@" ;;
+
+ help) shift; desktopctl.longhelp "$@" ;;
+ *) echo "desktopctl: '$*' is not a valid command"; desktopctl.help "$@" ;;
+esac
+