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

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
--menucommand-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.
The ipc-info.json File
Section titled “The ipc-info.json 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~/Library/Application Support/kando/ipc-info.json~/.config/kando/ipc-info.json~/.var/app/menu.kando.Kando/config/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.
Example code to find ipc-info.json
Section titled “Example code to find ipc-info.json”You can use a code snippet like the following to find the ipc-info.json file in a cross-platform way.
import osimport 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")Opening Predefined Menus
Section titled “Opening Predefined Menus”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.
Available Messages
Section titled “Available Messages”| Message Name | Sender | Description |
|---|---|---|
show-menu |
Client | Request to show a predefined menu. Pass the menu name via the name parameter. |
Example code to open a predefined menu
Section titled “Example code to open a predefined menu”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".
import asyncioimport jsonimport 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())Opening Custom Menus
Section titled “Opening Custom Menus”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.
Available Messages
Section titled “Available Messages”| 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. |
Message Arguments: path and interaction
Section titled “Message Arguments: path and interaction”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]}Example code to open a custom menu
Section titled “Example code to open a custom menu”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.
import asyncioimport jsonimport 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())Listening to Menu Interactions
Section titled “Listening to Menu Interactions”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.
Available Messages
Section titled “Available Messages”| 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 |
Example code to observe menu interactions
Section titled “Example code to observe menu interactions”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).
import asyncioimport jsonimport 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())Complete Code Examples
Section titled “Complete Code Examples”Below, you find two complete code examples for both opening a custom menu and observing menu interactions.
import asyncioimport jsonimport osimport platformimport 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())import asyncioimport jsonimport osimport platformimport websockets
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}" 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"}]}}, ] }
try: 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) msg_type = msg.get("type") if msg_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}")
elif msg_type == "error": print(f"Error: {msg.get('reason')} - {msg.get('description')}") break 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())import asyncioimport jsonimport osimport platformimport websockets
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": "start-observing"})) print("Started observing menu events...")
try: 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')}") except websockets.ConnectionClosed: print("Connection closed.") 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())Final Notes
Section titled “Final Notes”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.