Skip to content

Websocket Interface

banner

Kando offers a powerful Inter-Process Communication (IPC) API via WebSockets, enabling seamless integration with third-party applications and plugins.

This API allows you to both open menus in Kando and observe user interactions in any menu:

  • Opening Predefined Menus: Open one of your predefined menus by its name. This is similar to the --menu command-line option, but faster as it doesn’t require starting a new Kando instance.
  • Opening Custom Menus: Trigger Kando to display a menu you define, and receive the user’s selection. This is ideal for integrating Kando with other applications or workflows.
  • Listening to Menu Interactions: Observe when users open, hover, or select items in any Kando menu. This is useful for plugins that provide feedback (e.g., haptic feedback) or analytics.

All communication happens over a local WebSocket connection. To connect, you need to know the port and API version, which are provided by Kando in a special file.

Kando writes a file named ipc-info.json containing the WebSocket port and API version. Your client must read this file to connect to the IPC server. The location of this file depends on the operating system and installation method:

%appdata%\kando\ipc-info.json
{
"port": 12345,
"apiVersion": 2
}

Your client should always check this file before connecting, as the port will change between Kando sessions.

You can use a code snippet like the following to find the ipc-info.json file in a cross-platform way.

find_ipc_info.py
import os
import platform
def find_ipc_info_path():
system = platform.system()
home = os.path.expanduser("~")
if system == "Windows":
appdata = os.environ.get("APPDATA", os.path.join(home, "AppData", "Roaming"))
return os.path.join(appdata, "kando", "ipc-info.json")
elif system == "Darwin":
return os.path.join(home, "Library", "Application Support", "kando", "ipc-info.json")
else:
flatpak_path = os.path.join(home, ".var", "app", "menu.kando.Kando", "config", "kando", "ipc-info.json")
if os.path.exists(flatpak_path):
return flatpak_path
return os.path.join(home, ".config", "kando", "ipc-info.json")

Once you have the connection info, you can connect to the WebSocket server and send messages to control Kando. To open a predefined menu, send a show-menu message with the menu name as a parameter.

Message Name Sender Description
show-menu Client Request to show a predefined menu. Pass the menu name via the name parameter.

The code snippet below demonstrates how to open a predefined menu in Kando using Python. It reads the connection info from ipc-info.json, connects to the WebSocket server, and sends a show-menu message with the name "Example Menu".

open_predefined_menu.py
import asyncio
import json
import websockets
menu_name = "Example Menu"
async def main():
# Read ipc-info.json (see previous section for locating the file)
with open("/path/to/ipc-info.json") as f:
info = json.load(f)
uri = f"ws://127.0.0.1:{info['port']}"
async with websockets.connect(uri) as ws:
await ws.send(json.dumps({"type": "show-menu", "name": menu_name}))
if __name__ == "__main__":
asyncio.run(main())

To open a custom menu in Kando and receive the user’s selection, connect to the WebSocket server and send a show-custom-menu message. Kando will display your menu and send back events as the user interacts.

Message Name Sender Description
show-custom-menu Client Request to show a custom menu. Pass the menu structure via the menu parameter as JSON. The format of the menu is the same as the one used in the menus.json file. See Menu Configuration for more details.
menu-interaction Server The selected item is passed via the path parameter. The type of interaction via the interaction parameter. See below for details.
error Server An error occurred. Inspect the reason and description properties for more details.

The menu-interaction message contains two important arguments: path and interaction.

path is an array of zero-based integers representing the position of the item in the menu tree. For example, [2, 1] means the third child of the root, then the second child of that submenu.

interaction indicates what the user did. It can be one of the following: 'openMenu', 'openSubmenu', 'closeMenu', 'closeSubmenu', 'hoverParent', 'hoverCenter', 'hoverButton', 'hoverSubmenu', 'selectButton', 'activateMenu'.

Example

This means the user selected the third item in the first submenu.

{
"type": "menu-interaction",
"interaction": "selectButton",
"path": [0, 2]
}

The code snippet below demonstrates how to open a custom menu in Kando using Python. It reads the connection info from ipc-info.json, connects to the WebSocket server, and sends a show-custom-menu message with a simple menu structure. It then listens for user interactions and prints the selected item or cancellation.

open_custom_menu.py
import asyncio
import json
import websockets
async def main():
# Read ipc-info.json (see previous section for locating the file)
with open("/path/to/ipc-info.json") as f:
info = json.load(f)
uri = f"ws://127.0.0.1:{info['port']}"
menu = {
"type": "root",
"name": "Example Menu",
"icon": "🌸",
"iconTheme": "emoji",
"activateWorkflow": {"actions": [{"type": "close-menu"}]},
"children": [
{"type": "button", "name": "Item 1", "icon": "🚀", "iconTheme": "emoji", "selectWorkflow": {"actions": [{"type": "close-menu"}]}},
{"type": "button", "name": "Item 2", "icon": "💖", "iconTheme": "emoji", "selectWorkflow": {"actions": [{"type": "close-menu"}]}},
{"type": "button", "name": "Item 3", "icon": "🎇", "iconTheme": "emoji", "selectWorkflow": {"actions": [{"type": "close-menu"}]}},
{"type": "button", "name": "Item 4", "icon": "🎉", "iconTheme": "emoji", "selectWorkflow": {"actions": [{"type": "close-menu"}]}},
]
}
async with websockets.connect(uri) as ws:
await ws.send(json.dumps({"type": "show-custom-menu", "menu": menu}))
print("Menu sent. Waiting for user interaction...")
async for message in ws:
msg = json.loads(message)
if msg.get("type") == "menu-interaction":
path = msg.get("path", [])
interaction = msg.get("interaction")
if interaction == "selectButton":
print(f"Selected: {menu['children'][path[0]]['icon']}")
elif interaction == "closeMenu":
print("Menu closed.")
break
else:
print(f"Interaction: {interaction} at path {path}")
if __name__ == "__main__":
asyncio.run(main())

To observe all menu interactions (open, hover, select, cancel), of any menu opened by Kando, connect to the WebSocket server and send a start-observing message. Kando will then send you events as they happen. If you want to stop receiving events, send a stop-observing message.

Message Name Sender Description
start-observing Client Send this to start receiving menu interactions. events.
stop-observing Client Send this to stop receiving menu interaction events.
menu-interaction Server Same as above
error Server Same as above

The code snippet below demonstrates how to observe menu interactions in Kando using Python. It connects to the WebSocket server, sends a start-observing message, and prints events as they happen (menu opened, item hovered, item selected, menu canceled).

observe_interaction.py
import asyncio
import json
import websockets
async def main():
# Read ipc-info.json (see previous section for locating the file)
with open("/path/to/ipc-info.json") as f:
info = json.load(f)
uri = f"ws://127.0.0.1:{info['port']}"
async with websockets.connect(uri) as ws:
await ws.send(json.dumps({"type": "start-observing"}))
print("Started observing menu events...")
async for message in ws:
msg = json.loads(message)
msg_type = msg.get("type")
if msg_type == "menu-interaction":
interaction = msg.get("interaction")
path = msg.get("path", [])
print(f"Interaction: {interaction} at path {path}")
elif msg_type == "error":
print(f"Error: {msg.get('reason')} - {msg.get('description')}")
if __name__ == "__main__":
asyncio.run(main())

Below, you find two complete code examples for both opening a custom menu and observing menu interactions.

open_predefined_menu.py
import asyncio
import json
import os
import platform
import websockets
menu_name = "Example Menu"
def find_ipc_info_path():
system = platform.system()
home = os.path.expanduser("~")
if system == "Windows":
appdata = os.environ.get("APPDATA", os.path.join(home, "AppData", "Roaming"))
return os.path.join(appdata, "kando", "ipc-info.json")
elif system == "Darwin":
return os.path.join(home, "Library", "Application Support", "kando", "ipc-info.json")
else:
flatpak_path = os.path.join(home, ".var", "app", "menu.kando.Kando", "config", "kando", "ipc-info.json")
if os.path.exists(flatpak_path):
return flatpak_path
return os.path.join(home, ".config", "kando", "ipc-info.json")
async def main():
info_path = find_ipc_info_path()
if not os.path.exists(info_path):
print(f"ipc-info.json not found at {info_path}")
return
with open(info_path, 'r') as f:
info = json.load(f)
port = info['port']
api_version = info['apiVersion']
client_api_version = 2
if api_version != client_api_version:
print(f"API version mismatch: server={api_version}, client={client_api_version}")
return
uri = f"ws://127.0.0.1:{port}"
try:
async with websockets.connect(uri) as ws:
await ws.send(json.dumps({"type": "show-menu", "name": menu_name}))
except (ConnectionRefusedError, OSError) as e:
print(f"Could not connect to Kando IPC server at {uri}: Is Kando running?")
if __name__ == "__main__":
asyncio.run(main())

Have you created something cool with the IPC API? Share it with the community in the #show-and-tell channel on Kando’s Discord Server! We can also link to your project from here to inspire others and show what’s possible with Kando’s IPC interface.