diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..6a98487 --- /dev/null +++ b/Changelog.md @@ -0,0 +1,11 @@ +# Lima Xbar/Swiftbar changelog + +## 0.0.1 - 1.0.0 + +- `bash` proof of concept + +## 1.1.1 + +- Rewrite in Python for speed and maintainability. + - Now have submenus for container and image operations instead of just start/stop the VM + - We send notifications to the Notification Manager diff --git a/Makefile b/Makefile index f14d23a..9bc6484 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,20 @@ i: lint install -install: +install: format cp lima-plugin ~/Library/Application\ Support/xbar/plugins/lima-plugin.10s l: lint lint: - shellcheck lima-plugin \ No newline at end of file + shellcheck lima-plugin + +r: requirements +requirements: + poetry export -f requirements.txt --output requirements.txt + +f: format +format: + black lima-plugin *.py + +t: test +test: format + poetry run ./lima-plugin + diff --git a/README.md b/README.md index 953c2cc..a1ea532 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,11 @@ ![Screen shot of Lima menu with a running vm](https://raw.githubusercontent.com/unixorn/lima-xbar-plugin/main/pix/limactl-screen-shot.png) -This plugin is compatible with [xbar](https://xbarapp.com/) and [SwiftBar](https://github.com/swiftbar/SwiftBar), and lets you start and stop lima VMs from the menubar. +This plugin is compatible with [xbar](https://xbarapp.com/) and [SwiftBar](https://github.com/swiftbar/SwiftBar), and provides a menubar app that creates submenus for each Lima VM on your machine. For each VM, you can start/stop the VM, stop (and start or remove stopped containers) containers, and pull or remove images from the VM. ## Installation Copy `lima-plugin` to `~/Library/Application\ Support/xbar/plugins/lima-plugin.30s`, or run `make install` ### Dependencies -- [xbar](https://xbarapp.com/) - Allows you to make custom menubar apps with just scripts - -- [jq](https://stedolan.github.io/jq/) - `brew install jq` - Used to parse the output of `limactl` \ No newline at end of file +- [xbar](https://xbarapp.com/) or [SwiftBar](https://github.com/swiftbar/SwiftBar) - Both allow you to make custom menubar apps with simple scripts. diff --git a/lima-plugin b/lima-plugin index 1c3f399..7939a4c 100755 --- a/lima-plugin +++ b/lima-plugin @@ -1,6 +1,6 @@ -#!/usr/bin/env bash +#!/usr/bin/env python3 # -# XBar menu plugin for lima +# Lima Swiftbar/Xbar plugin # # Copyright 2021, Joe Block # @@ -12,422 +12,502 @@ # jq,lima # https://raw.githubusercontent.com/unixorn/lima-xbar-plugin/main/pix/limactl-screen-shot.png # https://github.com/unixorn/lima-xbar-plugin/ +# false # -# Dependencies: +# Dependencies: # lima - https://github.com/lima-vm/lima # jq - https://stedolan.github.io/jq/ +import argparse +import json +import logging +import logging.handlers +import os +import subprocess +import sys + +# import syslog + # Running VM color (default green) -RUNNING_VM_COLOR="#29cc00" +RUNNING_VM_COLOR = "#29cc00" # Stopped VM color (default red) -STOPPED_VM_COLOR="#ff0033" - -set -o pipefail -if [[ -n "$DEBUG" ]]; then - set -x -fi - -function cleanup() { - if [[ -d "$SCRATCH_D" ]]; then - rm -fr "$SCRATCH_D" - fi -} - -function debug() { - if [[ -n "$DEBUG" ]]; then - echo "$@" - fi -} - -function fail() { - printf '%s\n' "$1" >&2 ## Send message to stderr. Exclude >&2 if you don't want it that way. - exit "${2-1}" ## Return a code specified by $2 or 1 by default. -} - -function has() { - # Check if a command is in $PATH - which "$@" > /dev/null 2>&1 -} - -function displayAlert() { - alertCommand="display alert \"$1\" message \"$2\"" - osascript -e "$alertCommand" -} - -function displayNotification() { - if [[ $# -eq 1 ]]; then - message_command="display notification \"$1\"" - osascript -e "$message_command" - fi - - if [[ $# -eq 2 ]]; then - message_command="display notification \"$2\" with title \"$1\"" - osascript -e "$message_command" - fi - - if [[ $# -eq 3 ]]; then - message_command="display notification \"$3\" with title \"$1\" subtitle \"$2\"" - echo "message_command: $message_command" - osascript -e "$message_command" - fi - - if [[ $# -eq 4 ]]; then - message_command="display notification \"$4\" with title \"$1\" subtitle \"$2\" sound name \"$3\"" - osascript -e "$message_command" - fi -} - -# Set up a working scratch directory -SCRATCH_D=$(mktemp -d) - -if [[ ! "$SCRATCH_D" || ! -d "$SCRATCH_D" ]]; then - echo "Could not create temp dir" - exit 1 -fi - -trap cleanup EXIT - -export PATH="$PATH:/usr/local/bin" -XBAR_PLUGIN="$0" - -# shellcheck disable=SC2059 -function warnIfMissingDependencies() { - local depsOK=1 - local warnings="" - if ! has jq; then - warnings=$(printf "${warnings}\njq is not in your PATH! - click for installation instructions | color=$STOPPED_VM_COLOR href=https://stedolan.github.io/jq/download/\n") - depsOK=0 - fi - if ! has lima; then - warnings=$(printf "${warnings}lima is not in your PATH! - click to get started | color=$STOPPED_VM_COLOR href=https://github.com/lima-vm/lima#getting-started\n") - depsOK=0 - fi - if ! has limactl; then - warnings=$(printf "${warnings}limactl is not in your PATH! - click to get started | color=$STOPPED_VM_COLOR href=https://github.com/lima-vm/lima#getting-started\n") - depsOK=0 - fi - if ! has osascript; then - warnings=$(printf "${warnings}osascript is not in your PATH! - click to get started | color=$STOPPED_VM_COLOR\n") - depsOK=0 - fi - if [[ depsOK -eq 0 ]]; then - echo "🐋 ⛔ | color=$STOPPED_VM_COLOR" - echo '---' - echo "$warnings" - exit 0 - fi -} - -function printMenuBarIcon() { - # Bar title - local menuBarIcon - menuBarIcon="🐋 ⛔ | color=$STOPPED_VM_COLOR" - - for vm in $(limactl list --json | jq -r '.status') - do - if [[ "$vm" == 'Running' ]]; then - menuBarIcon="🐋 🏃 | color=$RUNNING_VM_COLOR" - fi - done - echo "$menuBarIcon" - echo '---' -} - -# shellcheck disable=SC2059 -function printMenu() { - warnIfMissingDependencies - - printMenuBarIcon - - local name - local vmstatus - - for raw in $(limactl list --json) - do - name=$(echo "$raw"| jq -r '.name' ) - vmstatus=$(echo "$raw" | jq -r '.status') - if [[ $vmstatus == 'Running' ]]; then - echo "$name VM is running | color=$RUNNING_VM_COLOR" - echo "--⛔ Stop $name VM | bash=$XBAR_PLUGIN param1=stop param2=$name terminal=false refresh=true" - - echo "-- Containers" - for container in $(vmContainers 'Up') - do - echo "---- $container" - echo "------ Running" - echo "-------- stop | bash=$XBAR_PLUGIN param1=stopContainer param2=$name param3=$container terminal=false refresh=true" - echo "-------- kill | bash=$XBAR_PLUGIN param1=killContainer param2=$name param3=$container terminal=false refresh=true" - echo "-------- pause | bash=$XBAR_PLUGIN param1=pauseContainer param2=$name param3=$container terminal=false refresh=true" - echo "-------- unpause | bash=$XBAR_PLUGIN param1=unpauseContainer param2=$name param3=$container terminal=false refresh=true" - done - for stopped in $(vmContainers 'Created') - do - echo "---- $stopped" - echo "------ Stopped" - echo "-------- rm | bash=$XBAR_PLUGIN param1=rmContainer param2=$name param3=$stopped terminal=false refresh=true" - echo "-------- start | bash=$XBAR_PLUGIN param1=startContainer param2=$name param3=$stopped terminal=false refresh=true" - done - for stopped in $(vmContainers 'Exited') - do - echo "---- $stopped" - echo "------ Stopped" - echo "-------- rm | bash=$XBAR_PLUGIN param1=rmContainer param2=$name param3=$stopped terminal=false refresh=true" - echo "-------- start | bash=$XBAR_PLUGIN param1=startContainer param2=$name param3=$stopped terminal=false refresh=true" - done - - echo "-- Images" - for image in $(vmImages) - do - echo "----$image" - echo "------ pull | bash=$XBAR_PLUGIN param1=pull param2=$name param3=$image terminal=false refresh=true" - echo "------ rm | bash=$XBAR_PLUGIN param1=rmImage param2=$name param3=$image terminal=false refresh=true" - done - else - echo "$name VM is stopped | color=$STOPPED_VM_COLOR" - echo "--▶️ Start $name VM | bash=$XBAR_PLUGIN param1=start param2=$name terminal=false refresh=true" - fi - done - - echo "force rescan | bash=limactl param1=list terminal=false refresh=true" - echo "Lima home | href=https://github.com/lima-vm/lima" - limactl --version -} - -function vmContainers() { - # $1 = status we're looking for - local wantedStatus - wantedStatus="${1}" - # default VM doesn't need to be specified - if [[ "$VM" != 'default' ]]; then - export LIMA_INSTANCE="$VM" - fi - # shellcheck disable=SC2001,SC2046,SC2005 - containerList="[$(echo $(lima nerdctl ps -a --format '{{json .}},') | sed 's/,$//')]" - # Can have spaces in our data, deal by using base64 (ugly) - for row in $(echo "${containerList}" | jq -r '.[] | @base64'); do - _jq() { - echo "${row}" | base64 --decode | jq -r "${1}" - } - if [[ $(_jq '.Status') == "$wantedStatus" ]]; then - name=$(_jq '.Names') - id=$(_jq '.ID') - if [[ "$name" != "" ]]; then - echo "$name" - else - echo "$id" - fi - fi - done -} - -function vmImages() { - # default VM doesn't need to be specified - if [[ "$VM" != 'default' ]]; then - export LIMA_INSTANCE="$VM" - fi - # shellcheck disable=SC2001,SC2046,2005 - imageList="[$(echo $(lima nerdctl images --format '{{json .}},') | sed 's/,$//')]" - # Can have spaces in our data, deal by using base64 (ugly) - for row in $(echo "${imageList}" | jq -r '.[] | @base64'); do - _jq() { - echo "${row}" | base64 --decode | jq -r "${1}" - } - echo "$(_jq '.Repository'):$(_jq '.Tag')" - done -} - -function pullImage() { - # arg1 = image - # arg2 = VM - local imageName - local VM - imageName="$1" - VM="$2" - if [[ "$VM" != 'default' ]]; then - export LIMA_INSTANCE="$VM" - fi - displayNotification lima "Pulling ${imageName} on ${VM}..." - if lima nerdctl image pull "${imageName}"; then - displayNotification Lima "Pulled ${imageName}" - else - displayAlert Lima "Failed to pull ${imageName} on ${VM}" - fi -} - -function rmImage() { - # arg1 = image - # arg2 = VM - local imageName - local VM - imageName="$1" - VM="$2" - if [[ "$VM" != 'default' ]]; then - export LIMA_INSTANCE="$VM" - fi - displayNotification lima "Removing ${imageName} from ${VM}..." - if lima nerdctl image rm "${imageName}"; then - displayNotification Lima "Removed ${imageName}" - else - displayAlert Lima "Failed to remove ${imageName}" - fi -} - -function startContainer() { - # arg1 = container - # arg2 = VM - local containerName - local VM - containerName="$1" - VM="$2" - if [[ "$VM" != 'default' ]]; then - export LIMA_INSTANCE="$VM" - fi - displayNotification lima "Starting ${containerName} on ${VM}..." - if lima nerdctl container start "${containerName}"; then - displayNotification Lima "Started ${containerName}" - else - displayAlert Lima "Failed to start ${containerName} on ${VM}" - fi -} - -function stopContainer() { - # arg1 = container - # arg2 = VM - local containerName - local VM - containerName="$1" - VM="$2" - if [[ "$VM" != 'default' ]]; then - export LIMA_INSTANCE="$VM" - fi - displayNotification lima "Stopping ${containerName} on ${VM}..." - if lima nerdctl container stop "${containerName}"; then - displayNotification Lima "Stopped ${containerName}" - else - displayAlert Lima "Failed to stop ${containerName} on ${VM}" - fi -} - -function killContainer() { - # arg1 = container - # arg2 = VM - local containerName - local VM - containerName="$1" - VM="$2" - if [[ "$VM" != 'default' ]]; then - export LIMA_INSTANCE="$VM" - fi - displayNotification lima "Killing ${containerName} on ${VM}..." - if lima nerdctl container kill "${containerName}"; then - displayNotification Lima "Killed ${containerName}" - else - displayAlert Lima "Failed to kill ${containerName} on ${VM}" - fi -} - -function rmContainer() { - # arg1 = container - # arg2 = VM - local containerName - local VM - containerName="$1" - VM="$2" - if [[ "$VM" != 'default' ]]; then - export LIMA_INSTANCE="$VM" - fi - displayNotification lima "Removing ${containerName} on ${VM}..." - if lima nerdctl container rm "${containerName}"; then - displayNotification Lima "Removed ${containerName}" - else - displayAlert Lima "Failed to remove ${containerName} on ${VM}" - fi -} - -function pauseContainer() { - # arg1 = container - # arg2 = VM - local containerName - local VM - containerName="$1" - VM="$2" - if [[ "$VM" != 'default' ]]; then - export LIMA_INSTANCE="$VM" - fi - displayNotification lima "Pausing ${containerName} on ${VM}..." - if lima nerdctl container pause "${containerName}"; then - displayNotification Lima "Paused ${containerName}" - else - displayAlert Lima "Failed to pause ${containerName} on ${VM}" - fi -} - -function unpauseContainer() { - # arg1 = container - # arg2 = VM - local containerName - local VM - containerName="$1" - VM="$2" - if [[ "$VM" != 'default' ]]; then - export LIMA_INSTANCE="$VM" - fi - displayNotification lima "Unpausing ${containerName} on ${VM}..." - if lima nerdctl container unpause "${containerName}"; then - displayNotification Lima "Unpaused ${containerName}" - else - displayAlert Lima "Failed to unpause ${containerName} on ${VM}" - fi -} - -function processMenuCommand() { - case "$1" in - images) - vmImages "$2" - ;; - pull) - pullImage "$3" "$2" # pull imagename vmname - ;; - startContainer) - startContainer "$3" "$2" # stopContainer containerName VMname - ;; - stopContainer) - stopContainer "$3" "$2" # stopContainer containerName VMname - ;; - killContainer) - killContainer "$3" "$2" # killContainer containerName VMname - ;; - pauseContainer) - pauseContainer "$3" "$2" # pauseContainer containerName VMname - ;; - rmContainer) - rmContainer "$3" "$2" # pauseContainer containerName VMname - ;; - unpauseContainer) - unpauseContainer "$3" "$2" # unpauseContainer containerName VMname - ;; - rmImage) - rmImage "$3" "$2" # pull imagename vmname - ;; - start) - displayNotification 'Lima' "Starting $2 VM" - if limactl start "$2"; then - displayNotification "Lima" "Started $2 VM successfully" - else - displayAlert "Lima" "Failed to start $2 VM" - fi - ;; - stop) - displayNotification 'Lima' "Stopping $2 VM" - if limactl stop "$2"; then - displayNotification "Lima" "Stopped $2 VM successfully" - else - displayAlert "Lima" "Failed to stop $2 VM" - fi - ;; - esac -} - -printMenu -processMenuCommand "$@" \ No newline at end of file +STOPPED_VM_COLOR = "#ff0033" + +VERSION = "1.1.1" + + +def logSetup(level: str = "INFO"): + maclog = logging.handlers.SysLogHandler( + address="/var/run/syslog", facility="local1" + ) + maclog.ident = "lima-xbar" + + loglevel = getattr(logging, level.upper(), None) + logFormat = " [%(asctime)s][%(levelname)8s][%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s" + logging.basicConfig(level=loglevel, format=logFormat) + + maclog.setLevel(loglevel) + + # set a format which is simpler for console use + formatter = logging.Formatter(" %(name)-12s: %(levelname)-8s %(message)s") + + # tell the handler to use this format + maclog.setFormatter(formatter) + + # add the handler to the root logger + logging.getLogger("").addHandler(maclog) + logging.debug("Set log level to %s", level.upper()) + + +def parseCLI(): + """ + Parse the command line options + """ + parser = argparse.ArgumentParser(description="Lima Swiftbar/Xbar plugin") + parser.add_argument("-d", "--debug", help="Debug setting", action="store_true") + parser.add_argument( + "-l", + "--log-level", + type=str.upper, + help="set log level", + choices=["DEBUG", "INFO", "ERROR", "WARNING", "CRITICAL"], + default="INFO", + ) + parser.add_argument( + "--vm", "--virtual-machine", type=str, help="Which vm to use", default="default" + ) + parser.add_argument("--target", type=str, help="Which image/vm/container to target") + + parser.add_argument( + "--container-action", + choices=["start", "stop", "rm", "pause", "unpause"], + help="Action to perform on a container", + ) + parser.add_argument( + "--image-action", choices=["pull", "rm"], help="Action to perform on image" + ) + parser.add_argument( + "--vm-action", + choices=["start", "stop"], + help="Action to perform on vm", + ) + cliArgs = parser.parse_args() + return cliArgs + + +# fun with osascript + + +def displayAlert(title: str, message: str): + """ + Display an alert using osascript. Blocking. + + :param str title: + :param str message: + """ + alertCommand = f'display alert "{title}" message "{message}"' + runCommand(command=["osascript", "-e", alertCommand]) + + +def displayNotification(title: str, message: str): + """ + Publish a notification to the notification manager. + + :param str title: + :param str message: + """ + alertCommand = f'display notification "{message}" with title "{title}" ' + runCommand(command=["osascript", "-e", alertCommand]) + + +def runCommand(command: list, env=dict(os.environ)): + """ + Run a command and decode the json output + + :param list command: + :return dict: + """ + return subprocess.run(command, env=env, stdout=subprocess.PIPE).stdout.decode( + "utf-8" + ) + + +def jsonCommand(command: list, env=dict(os.environ)): + """ + Run a command and decode the json output + + :param list command: + :return dict: + """ + json_output = runCommand(command=command, env=env) + + data = [] + for line in json_output.splitlines(): + try: + details = json.loads(line) + data.append(details) + except json.decoder.JSONDecodeError: + logging.error("Bad JSON returned: %s", line) + return data + + +def listContainers(vm: str = "default"): + """ + List all containers in a VM + + :param vm: + + :return dict: + """ + containers = {} + if vm != "default": + env = dict(os.environ, LIMA_INSTANCE=vm) + else: + env = dict(os.environ) + newpath = "%s:/usr/local/bin" % env["PATH"] + env["PATH"] = newpath + + command = [ + "lima", + "nerdctl", + "container", + "ls", + "-a", + "--format", + "{{json .}}", + ] + raw = jsonCommand(command=command, env=env) + for container in raw: + try: + if container["Names"] != "": + key = container["Names"] + else: + key = container["ID"] + containers[key] = container + except KeyError: + logging.error("Bad container record: %s", container) + return containers + + +def listImages(vm: str = "default"): + """ + List all images in a VM + + :param vm: + + :return dict: + """ + images = {} + if vm != "default": + env = dict(os.environ, LIMA_INSTANCE=vm) + else: + env = dict(os.environ) + + newpath = "%s:/usr/local/bin" % env["PATH"] + env["PATH"] = newpath + + command = ["lima", "nerdctl", "images", "--format", "{{json .}}"] + raw = jsonCommand(command=command, env=env) + logging.debug("Processing command output...") + for image in raw: + try: + repo = "ERROR" + tag = "ERROR" + if "Repository" in image: + repo = image["Repository"] + else: + logging.error("Repository key missing") + if "Tag" in image: + tag = image["Tag"] + else: + logging.error("Tag key missing") + images["%s:%s" % (repo, tag)] = image + except KeyError: + logging.error("Bad image record: %s", image) + logging.error("Keys: %s", image.keys()) + logging.error(" ") + return images + + +def listVMs(): + """ + List all VMs + + :return dict: + """ + vmList = {} + + env = dict(os.environ) + newpath = "%s:/usr/local/bin" % env["PATH"] + env["PATH"] = newpath + + vmRaw = subprocess.run( + ["/usr/local/bin/limactl", "list", "--json"], env=env, stdout=subprocess.PIPE + ).stdout.decode("utf-8") + + for vm in vmRaw.splitlines(): + details = json.loads(vm) + vmList[details["name"]] = details + return vmList + + +# Submenu processing + + +def prep_environment_for_lima(vm: str = "default", env: dict = dict(os.environ)): + """ + Set up an environment dictionary we can use to run a lima command. + + Also adds /usr/local/bin to $PATH + + :param str vm: VM to work in + :param dict env: Environment variables to base returned environment on + + :return dict: Environment dictionary, with /usr/local/bin added to $PATH + """ + newpath = "%s:/usr/local/bin" % env["PATH"] + env["PATH"] = newpath + + if vm != "default": + env["LIMA_INSTANCE"] = vm + return env + + +def containerOps(action: str, container: str, vm: str = "default"): + """ + Handle container operations + + :param str action: What container op to do + :param str container: What container to do the action on + :param str vm: Which VM is the container in? + """ + logging.warning("containerOps") + logging.debug("action: %s" % action) + logging.debug("container: %s" % container) + logging.debug("vm: %s" % vm) + + env = prep_environment_for_lima(vm=vm) + + command = ["lima", "nerdctl", "container", action, container] + logging.warning("containerops command: %s", command) + displayNotification(title="Lima VM", message=" ".join(command)) + + output = runCommand(command=command, env=env) + logging.warning(output) + logging.warning("%s complete", action) + displayNotification(title="Task complete", message=" ".join(command)) + + +def imageOps(action: str, image: str, vm: str = "default"): + """ + Handle VM operations + + :param str action: What image op to do + :param str image: What image to do the action on + :param str vm: Which VM is the image in? + """ + logging.critical("imageOps") + logging.info("action: %s" % action) + logging.info("image: %s" % image) + logging.info("vm: %s" % vm) + + env = prep_environment_for_lima(vm=vm) + + command = ["lima", "nerdctl", "image", action, image] + logging.warning("command: %s", command) + logging.warning("PATH: %s", env["PATH"]) + displayNotification(title="Lima VM", message=" ".join(command)) + output = runCommand(command=command, env=env) + logging.debug(output) + logging.warning("%s complete", action) + displayNotification(title="Task complete", message=" ".join(command)) + + +def vmOps(action: str, vm: str = "default"): + """ + Handle VM operations + + :param str action: What action to run - should be start or stop + :param str vm: Name of VM to act on + """ + logging.critical("vmOps") + logging.debug("action: %s" % action) + logging.debug("vm: %s" % vm) + + env = prep_environment_for_lima(vm=vm) + + command = ["limactl", action, vm] + logging.warning("command: %s", command) + displayNotification(title="Lima VM", message=" ".join(command)) + output = runCommand(command=command, env=env) + logging.debug(output) + logging.warning("%s complete", action) + displayNotification(title="Task completed", message=" ".join(command)) + + +# Actual Xbar-compatible output + + +def xbar_icon(vms: dict = {}): + """ + Determine icon to display in menubar. + + We display a running menubar icon if at least one VM is running. + + :param dict vms: Data about Lima VMs + """ + menuBarIcon = f"🐋 ⛔ | color={STOPPED_VM_COLOR}" + for vm in vms: + logging.debug("vm: %s", vm) + if vms[vm]["status"] == "Running": + menuBarIcon = f"🐋 🏃 | color={RUNNING_VM_COLOR}" + break + print(menuBarIcon) + print("---") + + +def aboutMenu(): + """ + Print details about plugin + """ + limaVersion = subprocess.run( + ["/usr/local/bin/limactl", "--version"], stdout=subprocess.PIPE + ).stdout.decode("utf-8") + + print("About…") + print("-- Lima version: %s" % limaVersion.strip()) + print("-- lima-xbar version: %s" % VERSION) + print("-- force rescan | bash=limactl param1=list terminal=false refresh=true") + + +def vmContainerSubMenu(vm: str = "default"): + """ + Generate a container submenu for a VM + + :param str vm: + """ + # plugin_f = __file__.replace(" ", "\ ") + plugin_f = __file__ + containers = listContainers(vm=vm) + + logging.debug("containers: %s", containers) + + print("-- Containers") + for container in containers: + if containers[container]["Status"] == "Up": + print("---- %s | color=%s" % (container, RUNNING_VM_COLOR)) + print("------ Running") + print( + f'------ stop | shell="{plugin_f}" param1="--vm" param2={vm} param3="--container-action" param4=stop param5="--target" param6={container} terminal=false refresh=true' + ) + print( + f'------ kill | shell="{plugin_f}" param1="--vm" param2={vm} param3="--container-action" param4=kill param5="--target" param6={container} terminal=false refresh=true' + ) + print( + f'------ pause | shell="{plugin_f}" param1="--vm" param2={vm} param3="--container-action" param4=pause param5="--target" param6={container} terminal=false refresh=true' + ) + else: + print("---- %s | color=%s" % (container, STOPPED_VM_COLOR)) + print("------ Stopped") + print( + f'------ rm | shell="{plugin_f}" param1="--vm" param2={vm} param3="--container-action" param4=rm param5="--target" param6={container} terminal=false refresh=true' + ) + print( + f'------ start | shell="{plugin_f}" param1="--vm" param2={vm} param3="--container-action" param4=start param5="--target" param6={container} terminal=false refresh=true' + ) + print( + f'------ unpause | shell="{plugin_f}" param1="--vm" param2={vm} param3="--container-action" param4=unpause param5="--target" param6={container} terminal=false refresh=true' + ) + + +def vmImageSubMenu(vm: str = "default"): + """ + Generate an image submenu for a VM + + :param str vm: + """ + plugin_f = __file__ + images = listImages(vm=vm) + + logging.debug("images: %s", images) + + print("-- Images") + for image in images: + print("---- %s" % image) + print( + f'------ pull | bash="{plugin_f}" param1=--vm param2={vm} param3=--image-action=pull param4=--target={image} terminal=false refresh=true' + ) + print( + f'------ rm | bash="{plugin_f}" param1=--vm param2={vm} param3=--image-action=rm param4=--target={image} terminal=false refresh=true' + ) + + +def vmMenu(vmData: dict = {}): + """ + Generate submenus for all the VMs, running or not + """ + plugin_f = __file__ + logging.debug("vmMenu") + logging.debug("vmData: %s", vmData) + + for vm in vmData: + + logging.debug("status %s", vmData[vm]["status"]) + if vmData[vm]["status"] != "Running": + print("%s VM is stopped | color=%s" % (vm, STOPPED_VM_COLOR)) + print( + f"""-- ⛔ Start {vm} VM | shell=\"{plugin_f}\" param1='--vm=default' param2='--vm-action=start' terminal=false refresh=true""" + ) + else: + print(f"{vm} VM (running) | color={RUNNING_VM_COLOR}") + print( + f"""-- ⛔ Stop {vm} VM | color={STOPPED_VM_COLOR} bash="{__file__}" shell=\"{plugin_f}\" param1='--vm=default' param2='--vm-action=stop' terminal=false refresh=true""" + ) + vmContainerSubMenu(vm=vm) + vmImageSubMenu(vm=vm) + + +def xbarMenu(): + """ + Generate Xbar Menu + """ + vms = listVMs() + + xbar_icon(vms) + aboutMenu() + vmMenu(vmData=vms) + + +def main(): + """ + Main program driver + """ + logSetup(level="DEBUG") + + logging.debug("plugin path: %s" % __file__) + + cli = parseCLI() + logging.warning("VERSON: %s", VERSION) + + logging.debug("cli: %s" % cli) + + logging.warning("argv[0] %s" % sys.argv[0]) + + if cli.container_action: + logging.info("container action: %s", cli.container_action) + containerOps(vm=cli.vm, action=cli.container_action, container=cli.target) + + if cli.image_action: + logging.info("image action: %s", cli.image_action) + imageOps(action=cli.image_action, image=cli.target, vm=cli.vm) + + if cli.vm_action: + logging.info("vm action: %s", cli.vm_action) + vmOps(action=cli.vm_action, vm=cli.vm) + + xbarMenu() + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..35d4e52 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,71 @@ +[[package]] +name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +toml = ["toml"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "bcca01ff6cd90504752451eb1aa2158eb0d3dc0cda53a73464ad06c9da0b07b2" + +[metadata.files] +coverage = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..56d70e5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.poetry] +name = "lima-plugin" +version = "0.1.0" +description = "Xbar and Swiftbar plugin to control lima" +authors = ["Joe Block "] +license = "Apache 2.0" + +[tool.poetry.dependencies] +python = "^3.9" +coverage = "^5.5" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api"