Skip to content

Menu API

Tomas Klaen edited this page Sep 5, 2024 · 15 revisions

open-menu <menu_json> [submenu_id]

A message other scripts can send to open a uosc menu serialized as JSON. You can optionally pass a submenu_id to pre-open a submenu. The ID is the submenu title chain leading to the submenu concatenated with >, for example Tools > Aspect ratio.

Menu data structure (pseudo types):

MenuBase {
  title?: string;
  items: Child[];
  selected_index?: integer;
  keep_open?: boolean;
  footnote?: string; // Short message below the menu.
  id?: string; // Default IDs look like `{root} > Submenu title`. You can overwrite it with this.
  on_search?: 'callback' | string | string[]; // If command, query & menu_id added as last params.
  on_paste?: 'callback' | string | string[]; // If command, value & menu_id added as last params.
  on_move?: 'callback' | string | string[]; // If command, from_index, to_index, and menu_id added as last params.
  search_style?: 'on_demand' | 'palette' | 'disabled'; // default: on_demand
  search_debounce?: 'submit' | number; // default: 0
  search_suggestion?: string;
  search_submenus?: boolean;
  item_actions?: Action[];
  item_actions_place?: 'inside'|'outside'; // Preferred buttons place. Default: 'inside'
}

Menu extends MenuBase {
  type?: string;
  on_close?: 'callback' | string | string[];
  callback?: string[];
}

Submenu extends MenuBase {
  hint?: string;
  bold?: boolean;
  italic?: boolean;
  align?: 'left'|'center'|'right';
  muted?: boolean;
  separator?: boolean;
}

Child = Item | Submenu;

Item {
  title?: string;
  hint?: string;
  icon?: string;
  value: string | string[];
  active?: integer;
  selectable?: boolean;
  bold?: boolean;
  italic?: boolean;
  align?: 'left'|'center'|'right';
  muted?: boolean;
  separator?: boolean;
  keep_open?: boolean;
  actions?: Action[];
  actions_place?: 'inside'|'outside'; // Preferred buttons place. Default: 'inside'
}

Action {
  name: string;
  icon: string;
  label?: string;
}

It's not necessary to define selected_index as it'll default to the first active item, or 1st item in the list.

When Item.value is a string, it'll be passed to mp.command(value). If it's a table (array) of strings, it'll be used as mp.commandv(table.unpack(value)). The same goes for on_close and on_search. on_search additionally appends the current search string as the last parameter.

on_close is only sent if uosc/user is closing the menu. It is NOT send after you called close-menu, as that might lead to circular loops.

Menu.type is used to refer to this menu in update-menu and close-menu. While the menu is open this value will be available in user-data/uosc/menu/type and the shared-script-properties entry uosc-menu-type. If no type was provided, those will be set to 'undefined'.

search_style can be:

  • on_demand (default) - Search input pops up when user starts typing, or presses / or ctrl+f, depending on user configuration. It disappears on shift+backspace, or when input text is cleared.
  • palette - Search input is always visible and can't be disabled. In this mode, menu title is used as input placeholder when no text has been entered yet.
  • disabled - Menu can't be searched.

search_debounce controls how soon the search happens after the last character was entered in milliseconds. Entering new character resets the timer. Defaults to 300. It can also have a special value 'submit', which triggers a search only after ctrl+enter was pressed.

search_submenus makes uosc's internal search handler (when no on_search callback is defined) look into submenus as well, effectively flattening the menu for the duration of the search. This property is inherited by all submenus.

search_suggestion fills menu search with initial query string. Useful for example when you want to implement something like subtitle downloader, you'd set it to current file name. item.icon property accepts icon names. You can pick one from here: Google Material Icons
There is also a special icon name spinner which will display a rotating spinner. Along with a no-op command on an item and keep_open=true, this can be used to display placeholder menus/items that are still loading.

on_paste is triggered when user pastes a string while menu is opened. Works the same as on_search.

When keep_open is true, activating the item will not close the menu. This property can be defined on both menus and items, and is inherited from parent to child if child doesn't overwrite it.

MenuBase.item_actions & Item.actions adds buttons to items in the menu. You can use Item.actions to add different buttons to each individual item, or MenuBase.item_actions to add same actions to all items of that menu. The information about what action was pressed is only available via Callback mode documented below. If user presses the button instead of the item, its name will be available on callback's event.action property, otherwise it's nil.

MenuBase.item_actions_place & Item.actions_place control whether the preferred place for action buttons is inside or outside the menu. Default is inside. You should use outside when your menu has hints/icons that have a high informational value and shouldn't be covered by buttons unless necessary. Note that uosc will still place buttons inside the menu if there's not enough space for them outside.

callback enables a more advanced API interfacing. See Callback mode below for more.

Example:

local utils = require('mp.utils')
local menu = {
  type = 'menu_type',
  title = 'Custom menu',
  items = {
    {title = 'Foo', hint = 'foo', value = 'quit'},
    {title = 'Bar', hint = 'bar', value = 'quit', active = true},
  }
}
mp.commandv('script-message-to', 'uosc', 'open-menu', utils.format_json(menu))

update-menu <menu_json>

Updates currently opened menu with the same type.

The difference between this and open-menu is that if the same type menu is already open, open-menu will reset the menu as if it was newly opened, while update-menu will update it's data.

update-menu, along with {menu/item}.keep_open property and item command that sends a message back can be used to create a self updating menu with some limited UI. Example:

local utils = require('mp.utils')
local script_name = mp.get_script_name()
local state = {
  checkbox = 'no',
  radio = 'bar'
}

function command(str)
  return string.format('script-message-to %s %s', script_name, str)
end

function create_menu_data()
  return {
    type = 'test_menu',
    title = 'Test menu',
    keep_open = true,
    items = {
      {
        title = 'Checkbox',
        icon = state.checkbox == 'yes' and 'check_box' or 'check_box_outline_blank',
        value = command('set-state checkbox ' .. (state.checkbox == 'yes' and 'no' or 'yes'))
      },
      {
        title = 'Radio',
        hint = state.radio,
        items = {
          {
            title = 'Foo',
            icon = state.radio == 'foo' and 'radio_button_checked' or 'radio_button_unchecked',
            value = command('set-state radio foo')
          },
          {
            title = 'Bar',
            icon = state.radio == 'bar' and 'radio_button_checked' or 'radio_button_unchecked',
            value = command('set-state radio bar')
          },
          {
            title = 'Baz',
            icon = state.radio == 'baz' and 'radio_button_checked' or 'radio_button_unchecked',
            value = command('set-state radio baz')
          },
        },
      },
      {
        title = 'Submit',
        icon = 'check',
        value = command('submit'),
        keep_open = false
      },
    }
  }
end

mp.add_forced_key_binding('t', 'test_menu', function()
  local json = utils.format_json(create_menu_data())
  mp.commandv('script-message-to', 'uosc', 'open-menu', json)
end)

mp.register_script_message('set-state', function(prop, value)
  state[prop] = value
  -- Update currently opened menu
  local json = utils.format_json(create_menu_data())
  mp.commandv('script-message-to', 'uosc', 'update-menu', json)
end)

mp.register_script_message('submit', function(prop, value)
  -- Do something with state
end)

select-menu-item <menu_type> <item_index> [submenu_id]

Selects an item in menu and immediately scrolls to it. Ignored if user is currently navigating with a pointer and not a keyboard.

  • <menu_type> required - Your menu type. Ensures you won't be messing with other menus.
  • <item_index> required - Index of an item to select.
  • [submenu_id] optional - ID of (sub)menu in nested menus. When empty selects item in currently active (sub)menu.

close-menu [type]

Closes the menu. If the optional parameter type is provided, then the menu only closes if it matches Menu.type of the currently open menu.


Callback mode

Provides more control over menu lifecycle via sending detailed events to callback, and allowing it to decide what happens afterwards. It's also the only way to access what action button was pressed when activating an item.

Menu.callback property value are params to be used in script-message-to command before event data to reach your message handler:

-- Assuming `callback = {'foo', 'bar'}`, each event will be triggered with:
mp.commandv('script-message-to', 'foo', 'bar', event_data)

Not all events are redirected to callback automatically, some need to be specifically configured to do so by setting their on_{event} config to 'callback'. Here's a table:

Event Condition Note
activate always Any enter or primary click with any modifier combination is turned into activate event.
You need to send close-menu message to close the menu.
move on_move='callback' Condition informs us that you're handling this event, so we can adjust selected item index. (ctrl+up/down/pgup/pgdwn/home/end)
search on_search='callback' Condition informs us that you're handling this event, so we redirect search handling to callback. (ctrl+enter)
key always Only keys that don't already have a function are sent. You'll get enter (and its modifier combinations) only if no item is selected and user presses enter key, otherwise it turns into activate event.
Due to environment limitations, we don't listen to every shortcut possible. You can see which ones are bound by reading the Menu:enable_key_bindings() function source code in src/uosc/elements/Menu.lua.
paste on_paste='callback' Condition informs us that you're handling this event, so we don't start search on paste, but redirect it to callback. (ctrl+v)
back always Fired when user tries to navigate back in submenu structure when there's no parent menu. (backspace)
close on_close='callback' When handled by callback and user tries to close the menu, it'll instead send a close event, and you have to close by sending close-menu message. (esc, or primary mouse button on background)

event_data is a json encoded string with one of these interfaces (pseudo types):

EventActivate {
  type: 'activate';
  index: number;
  value: any;
  action?: string;
  keep_open?: boolean; // Inherited from item or its menu prop.
  modifiers?: string; // Combined modifiers ID string, e.g.: `alt+ctrl` - lowercase & in alphabetical order.
  alt: boolean;
  ctrl: boolean;
  shift: boolean;
  is_pointer: boolean; // Whether the event was triggered by a pointer click/tap.
  menu_id: string;
}
EventMove {type: 'move'; from_index: number; to_index: number; menu_id: string;}
EventSearch {type: 'search'; query: string; menu_id: string;}
EventKey {
  type: 'key';
  id: string; // e.g.: `alt+ctrl+enter` always lowercase, modifiers in alphabetical order, key last.
  key: string; // e.g.: `enter`. Note: mouse primary is normalized to `enter`.
  modifiers?: string; // e.g.: `alt+ctrl` always lowercase & in alphabetical order.
  alt: boolean;
  ctrl: boolean;
  shift: boolean;
  menu_id: string;
  selected_item?: {index: number; value: any; action?: string;}
}
EventPaste {
  type: 'paste';
  value: string;
  menu_id: string;
  selected_item?: {index: number; value: any; action?: string;}
}
EventBack {type: 'back';}
EventClose {type: 'close';}

Example:

local utils = require('mp.utils')
local menu = {
  type = 'menu_type',
  title = 'Custom menu',
  callback = {mp.get_script_name(), 'menu-event'},
  actions = {
    {name = 'thumb_up', icon = 'thumb_up', label = 'Thumbs up'},
  },
  items = {
    {title = 'Foo', hint = 'foo', value = 'foo'},
    {title = 'Bar', hint = 'bar', value = 'bar', active = true},
  }
}

-- Open menu
mp.commandv('script-message-to', 'uosc', 'open-menu', utils.format_json(menu))

-- Handle events
mp.register_script_message('menu-event', function(json)
  local event = utils.parse_json(json)
  if event.type == 'activate' then
    print(event.value) -- 'foo' | 'bar'
    print(event.action) -- nil | 'thumb_up'
    mp.commandv('script-message-to', 'uosc', 'close-menu', 'menu_type')
  end
end)