Skip to content

Menu API

Tomas Klaen edited this page Jul 23, 2024 · 17 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: Item[];
  selected_index?: integer;
  keep_open?: boolean;
  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;
  actions?: Action[];
}

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;
}

Item = Command | Submenu;

Command {
  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[];
}

Action {
  name: string;
  icon: string;
  tooltip?: 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 Command.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 Menu.on_close and on_search. on_search additionally appends the current search string as the last parameter.

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.

actions adds buttons to items in the menu. It can be on each item if each needs different buttons, or just on a menu to add same buttons to all items. If user presses the button instead of the item, its name will be available on callback's event.action property, otherwise it's nil. You can only get this data by using the callback interface documented below.

callback enables gradual takeover of the menu lifecycle. Most event will be sent to be handled by your callback, and the rest (close, search, move) can be configured to do so as well (documented below). Value are params to be used in script-message-to command before event data to reach your message handler (mp.commandv('script-message-to', callback[1], ...callback[n], event_data)). Assuming callback handler is defined, here is a table of what events it receives and under what conditions:

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)
remove always Request for item to be removed from list. (ctrl+del)
search on_search='callback' Condition informs us that you're handling this event, so we redirect our 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, menu won't close on its own, you have to do it manually 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;
  menu_id: string;
}
EventMove {type: 'move'; from_index: number; to_index: number; menu_id: string;}
EventRemove {type: 'remove'; index: number; value: any; 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` 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',
  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))

Callback and item actions example:

local utils = require('mp.utils')
local menu = {
  type = 'menu_type',
  title = 'Custom menu',
  callback = {mp.get_script_name(), 'menu-event'},
  actions = {
    {name = 'thumbs_up', icon = 'thumbs_up', title = 'Tooltip text'},
  },
  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(data.value) -- 'foo' | 'bar'
    print(data.action) -- nil | 'thumbs_up'
  end
end)

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)

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.

Clone this wiki locally