{"action":"created","comment":{"url":"https://api.github.com/repos/luakit/luakit-plugins/pulls/comments/156656702","pull_request_review_id":83156956,"id":156656702,"diff_hunk":"@@ -0,0 +1,870 @@\n+--- Tab groups management module\n+--\n+-- This module allows you to group opened tabs and switch between different groups\n+-- and tabs in groups\n+--\n+-- # Capabilities\n+--\n+-- # Usage\n+--\n+-- * Add `require \"tabgroups\"` to your `config.rc`.\n+-- * Press 'x' to open list of defined tabgroups\n+-- * Press 'X' to open list of tabs in active tabgroup\n+--\n+-- # Troubleshooting\n+--\n+-- # Files and Directories\n+--\n+--\n+-- @module tabgroups\n+-- @author Serg Kozhemyakin <serg.kozhemyakin@gmail.com>\n+-- @author Aidan Holm <aidanholm@gmail.com>\n+-- @copyright 2017 Serg Kozhemyakin <serg.kozhemyakin@gmail.com>\n+\n+local string = require(\"string\")\n+local window = require(\"window\")\n+local webview = require(\"webview\")\n+local binds, modes = require(\"binds\"), require(\"modes\")\n+local add_binds = modes.add_binds\n+local menu_binds = binds.menu_binds\n+local new_mode = require(\"modes\").new_mode\n+local session = require(\"session\")\n+local settings = require(\"settings\")\n+local lousy = require(\"lousy\")\n+\n+local _new_tabgroup_prefix = \"Unnamed#\"\n+local _default_notify = \"n: create new group, d: delete group, r: rename group\"\n+\n+-- private hash for storing map uri to tabgroup\n+local w2groups = setmetatable({}, { __mode = \"k\" })\n+\n+-- temporary table for storing deleted notebooks, will be cleaned at idle time\n+local _deleted_groups = {}\n+\n+local switch_tabgroup, delete_tabgroup\n+\n+local function _get_next_tabgroup_name(w)\n+    local name\n+    local i = 1\n+    if w2groups[w] and w2groups[w].groups then\n+        repeat\n+            i = i + 1\n+            name = _new_tabgroup_prefix .. i\n+        until not w2groups[w].groups[name]\n+    else\n+        name = _new_tabgroup_prefix .. i\n+    end\n+    return name\n+end\n+\n+local function grouptabs(w, group)\n+    assert(type(group) == 'string')\n+    assert(w2groups[w].groups[group])\n+    assert(w2groups[w].groups[group]._notebook)\n+    assert(w2groups[w].groups[group]._notebook.type == \"notebook\")\n+    local i = 0\n+    local n = w2groups[w].groups[group]._notebook:count()\n+    return function()\n+        i = i + 1\n+        if i <= n then return i, w2groups[w].groups[group]._notebook[i] end\n+    end\n+end\n+\n+local function webview2idx(w, group, view)\n+    for idx, v in grouptabs(w, group) do\n+        if v == view then\n+            return idx\n+        end\n+    end\n+    return nil\n+end\n+\n+-- return table with tabgroup info\n+local function webview2group(view)\n+    for w, _ in pairs(w2groups) do\n+        if w2groups[w] and w2groups[w].groups then\n+            for g, _ in pairs(w2groups[w].groups) do\n+                for _, v in grouptabs(w, g) do\n+                    if v == view then\n+                        return w2groups[w].groups[g]\n+                    end\n+                end\n+            end\n+        end\n+    end\n+    return nil\n+end\n+\n+-- return table with tabgroup info\n+local function tablist2group(nb)\n+    for w, _ in pairs(w2groups) do\n+        for _, gv in pairs(w2groups[w].groups) do\n+            if nb == gv._notebook then\n+                return gv\n+            end\n+        end\n+    end\n+    return nil\n+end\n+\n+local function current_webview_in_group(w, group)\n+    local ret = nil\n+    if w2groups[w] and w2groups[w].groups[group] then\n+        ret = w2groups[w].groups[group]._notebook[w2groups[w].groups[group]._notebook:current()]\n+    end\n+    return ret\n+end\n+\n+local function number_of_tabgroups(w)\n+    local n = 0\n+    for _, _ in pairs(w2groups[w].groups) do\n+        n = n + 1\n+    end\n+    return n\n+end\n+\n+local function _page_added_cb(nb, view)\n+    local g = tablist2group(nb)\n+    assert(g)\n+    assert(not g.tabs[view])\n+    g.tabs[view] = { tab_hit = 0, atime = 0, ctime = os.time(), mtime = 0 }\n+    g.mtime = os.time();\n+    view:add_signal(\"property::uri\", function (v)\n+        local grp = webview2group(v)\n+        assert(grp)\n+        grp.tabs[v].mtime = os.time()\n+    end)\n+end\n+\n+local function _page_removed_cb(nb, view)\n+    local g = tablist2group(nb)\n+    if g then\n+        if g.tabs[view] then\n+            g.tabs[view] = nil\n+        end\n+        g.mtime = os.time();\n+    end\n+end\n+\n+local function _switch_page_cb(nb, view)\n+    local g = tablist2group(nb)\n+    assert(g)\n+    assert(g.tabs[view])\n+    g.tabs[view].tab_hit = g.tabs[view].tab_hit + 1\n+    g.tabs[view].atime = os.time()\n+end\n+\n+local function add_signals_to_notebook(nb)\n+    nb:add_signal(\"page-added\", _page_added_cb)\n+    nb:add_signal(\"page-removed\", _page_removed_cb)\n+    nb:add_signal(\"switch-page\", _switch_page_cb)\n+end\n+\n+-- return tabgroup table, newly created or already existing\n+local function create_tabgroup(w, group_name)\n+    if not w2groups[w].groups[group_name] then\n+        local nt = widget({type=\"notebook\"})\n+        w.tabs.parent:insert(nt)\n+        nt.show_tabs = false\n+\n+        w2groups[w].groups[group_name] = { name = group_name,\n+            _notebook = nt,\n+            group_hit = 0,\n+            atime = 0,\n+            mtime = 0,\n+            ctime = os.time(),\n+            tabs = {} }\n+\n+        add_signals_to_notebook(nt)\n+    end\n+    return w2groups[w].groups[group_name]\n+end\n+\n+local function _select_next_opened_tabgroup(w, group)\n+    local switch_to = nil\n+    local tg_list, n, idx = {}, 1, -1\n+    for name, _ in pairs(w2groups[w].groups) do\n+        tg_list[n] = name\n+        if name == group then idx = n end\n+        n = n + 1\n+    end\n+    if idx > 0 then\n+        if idx == 1 then\n+            switch_to = tg_list[idx+1]\n+        else\n+            switch_to = tg_list[idx-1]\n+        end\n+    end\n+    return switch_to\n+end\n+\n+local function open_new_tab_in_tabgroup (w, group, uri, opts)\n+    local tg = create_tabgroup(w, group)\n+    opts = opts or {}\n+    if tg then\n+        local view = webview.new({ private = opts.private })\n+        if opts.session_restore then\n+            webview.modify_load_block(view, \"tabgroups-restore\", true)\n+            local function unblock(vv)\n+                webview.modify_load_block(vv, \"tabgroups-restore\", false)\n+                vv:remove_signal(\"switched-page\", unblock)\n+            end\n+            view:add_signal(\"switched-page\", unblock)\n+        end\n+        -- copy/pasted from attach_tab function in window module\n+        local order = opts.order\n+        local taborder = package.loaded.taborder\n+        if not order and taborder then\n+            order = (opts.switch == false and taborder.default_bg)\n+                or taborder.default\n+        end\n+        local pos = tg._notebook:insert((order and order(w, view)) or -1, view)\n+        assert(tg.tabs[view])\n+        assert(tg._notebook)\n+        webview.set_location(view, { session_state = opts.session_state, uri = uri or opts.uri })\n+        if opts.switch then\n+            tg._notebook:switch(pos)\n+        end\n+    end\n+end\n+\n+local function _cleaner()\n+    for k, g in pairs(_deleted_groups) do\n+        if g and g._notebook then\n+            g._notebook:remove_signal(\"page-removed\", _page_removed_cb)\n+            g._notebook:remove_signal(\"page-added\", _page_added_cb)\n+            g._notebook:remove_signal(\"switch-page\", _switch_page_cb)\n+            local i = 0\n+            local n = g._notebook:count()\n+            while i <= n do\n+                i = i + 1\n+                local v = g._notebook[i]\n+                if v then\n+                    g._notebook:remove(v)\n+                end\n+            end\n+            g._notebook = nil\n+        end\n+        _deleted_groups[k] = nil\n+    end\n+    return false\n+end\n+\n+window.add_signal(\"init\", function (w)\n+    local group_name = _get_next_tabgroup_name(w)\n+    w2groups[w] = { active = group_name , groups = { }  }\n+\n+    local _nb = w.tabs\n+    local group_nb = widget{type=\"notebook\"}\n+    group_nb.show_tabs = false\n+    _nb:replace(group_nb)\n+    group_nb:insert(_nb)\n+\n+    w2groups[w].groups[group_name] = { name = group_name,\n+        _notebook = w.tabs,\n+        group_hit = 0,\n+        atime = os.time(),\n+        ctime = os.time(),\n+        tabs = {} }\n+\n+    add_signals_to_notebook(w.tabs)\n+\n+    w:add_signal(\"detach-tab\", function (win, _)\n+        local current_tg_name = w2groups[win].active\n+        local nb = w2groups[win].groups[current_tg_name]._notebook\n+        if nb:count() == 1 then\n+            -- if we closing last tab in active tabgroup -- let's switch to other tabgroup\n+            -- and remove current one\n+            if number_of_tabgroups(win) > 1 then\n+                switch_tabgroup(win, _select_next_opened_tabgroup(w, current_tg_name))\n+            end\n+            delete_tabgroup(win, current_tg_name)\n+        end\n+    end)\n+end)\n+\n+-- session handling\n+session.add_signal(\"restore\", function (state)\n+    for w, win_state in pairs(state) do\n+        if win_state.tab_groups then\n+            -- let's rename default group as active one, if name is different\n+            if win_state.tab_groups.active ~= w2groups[w].active then\n+                w2groups[w].groups[win_state.tab_groups.active] = w2groups[w].groups[w2groups[w].active]\n+                w2groups[w].groups[w2groups[w].active] = nil\n+                w2groups[w].active = win_state.tab_groups.active\n+                w2groups[w].groups[win_state.tab_groups.active].name = win_state.tab_groups.active\n+            end\n+\n+            for gn, g in pairs(win_state.tab_groups.groups) do\n+                for i, s in ipairs(g.tabs) do\n+                    if gn ~= w2groups[w].active then\n+                        open_new_tab_in_tabgroup(w, gn, s.uri,  {switch = g.active == i,\n+                            session_state = s.session_state,\n+                            session_restore = true })\n+                    end\n+                    if s[i] then\n+                        w2groups[w].groups[gn].tabs[i]  = { tab_hit = s[i].tab_hit,\n+                            atime = s[i].atime,\n+                            ctime = s[i].ctime,\n+                            mtime = s[i].mtime }\n+                    else\n+                        w2groups[w].groups[gn].tabs[i]  = { tab_hit = 0,\n+                            atime = 0,\n+                            mtime = 0,\n+                            ctime = os.time() }\n+                    end\n+                end\n+                w2groups[w].groups[gn].name = win_state.tab_groups.groups[gn].name\n+                w2groups[w].groups[gn].atime  = win_state.tab_groups.groups[gn].atime or 0\n+                w2groups[w].groups[gn].atime  = win_state.tab_groups.groups[gn].atime or 0\n+                w2groups[w].groups[gn].mtime  = win_state.tab_groups.groups[gn].mtime or 0\n+                w2groups[w].groups[gn].ctime  = win_state.tab_groups.groups[gn].ctime or os.time()\n+            end\n+        end\n+    end\n+end)\n+\n+session.add_signal(\"save\", function(state)\n+    local wins = lousy.util.table.values(window.bywidget)\n+    for _, w in ipairs(wins) do\n+        state[w].tab_groups = { active = w2groups[w].active, groups = {} }\n+        for gn, g in pairs(w2groups[w].groups) do\n+            local _a = current_webview_in_group(w, gn)\n+            state[w].tab_groups.groups[gn] = { name = gn,\n+                group_hit = g.group_hit,\n+                atime = g.atime,\n+                ctime = g.ctime,\n+                mtime = g.mtime,\n+                tabs = {} }\n+            for i, v in grouptabs(w, gn) do\n+                assert(g.tabs[v])\n+                state[w].tab_groups.groups[gn].tabs[i] = { tab_hit = g.tabs[v].tab_hit or 0,\n+                    atime = g.tabs[v].atime,\n+                    mtime = g.tabs[v].mtime,\n+                    ctime = g.tabs[v].ctime,\n+                }\n+                -- we don't store uri and state for tabs in active tg because this info already stored\n+                -- by luakit session manager\n+                if gn ~= w2groups[w].active then\n+                    state[w].tab_groups.groups[gn].tabs[i].uri = v.uri\n+                    state[w].tab_groups.groups[gn].tabs[i].session_state = v.session_state\n+                end\n+                if v == _a then\n+                    state[w].tab_groups.groups[gn].active = i\n+                end\n+            end\n+        end\n+    end\n+    return state\n+end)\n+\n+local function _sort_by_field(field, order, a, b)\n+    assert(field)\n+    assert(order)\n+    assert(a[field])\n+    assert(b[field])\n+    local va, vb = a[field], b[field]\n+    local ret\n+    if va <= vb then\n+        ret = true\n+    else\n+        ret = false\n+    end\n+    if order == 'desc' then\n+        ret = not ret\n+    end\n+    return ret\n+end\n+\n+local function _build_tabgroup_menu_grouptabs(w, group_name, field, order)\n+    local rows = {{\"Group name\", \"Tab title\", \"URI\", title = true}}\n+    local active = current_webview_in_group(w, group_name)\n+\n+    local _tmp = {}\n+    local _gv = w2groups[w].groups[group_name]\n+    for i, v in grouptabs(w, group_name) do\n+        if not _gv.tabs[v] then\n+            _gv.tabs[v] = { tab_hit = 1, atime = os.time(), ctime = os.time() }\n+        end\n+        table.insert(_tmp, {v = v,\n+            hits = _gv.tabs[v].tab_hit,\n+            atime = _gv.tabs[v].atime,\n+            mtime = _gv.tabs[v].mtime,\n+            ctime = _gv.tabs[v].ctime,\n+            title = v.title,\n+            n = i })\n+    end\n+    if field and order then\n+        table.sort(_tmp, function(a, b) return _sort_by_field(field, order, a, b) end)\n+    end\n+\n+    for i, v in ipairs(_tmp) do\n+        local title = '*No title*'\n+        if v.title then\n+            title = v.v.title\n+        end\n+        if v.v == active then\n+            table.insert(rows, {\"<b>\"..((i < 10 and i..' - ') or '')..group_name..\"</b>\",\n+                \"<b>\"..lousy.util.escape(title)..\"</b>\",\n+                lousy.util.escape(v.v.uri),\n+                _group = group_name, _tab = v.v})\n+        else\n+            table.insert(rows, {((i < 10 and i..' - ') or '')..lousy.util.escape(group_name),\n+                lousy.util.escape(title),\n+                lousy.util.escape(v.v.uri),\n+                _group = group_name, _tab = v.v})\n+        end\n+    end\n+    return rows\n+end\n+\n+local function _build_tabgroup_menu_grouplist(w, field, order)\n+    local rows = {{ \"Group name\", \"Number of tabs\", title = true }}\n+\n+    local _tmp = {}\n+    for g, gv in pairs(w2groups[w].groups) do\n+        table.insert(_tmp, {name = g,\n+            hits = gv.group_hit,\n+            atime = gv.atime,\n+            mtime = gv.mtime,\n+            ctime = gv.ctime,\n+        })\n+    end\n+    if field and order then\n+        table.sort(_tmp, function(a, b) return _sort_by_field(field, order, a, b) end)\n+    end\n+    for i, g in ipairs(_tmp) do\n+        if g.name == w2groups[w].active then\n+            table.insert(rows, { \"<b>\"..((i < 10 and i..' - ') or '')..lousy.util.escape(g.name)..\"</b>\",\n+                w2groups[w].groups[g.name]._notebook:count(),\n+                _group = g.name})\n+        else\n+            table.insert(rows, { ((i < 10 and i..' - ') or '')..lousy.util.escape(g.name),\n+                w2groups[w].groups[g.name]._notebook:count(),\n+                _group = g.name})\n+        end\n+    end\n+    return rows\n+end\n+\n+-- visual mode\n+local function build_tabgroup_menu(w, expand_group)\n+    local sort = settings.get_setting(\"tabgroups.sort_groups_by\")\n+    local field, order = nil, nil\n+    if expand_group then\n+        sort = settings.get_setting(\"tabgroups.sort_tabs_by\")\n+    end\n+    if sort then\n+        field, order = string.match(sort, \"^%s*(%w+)%s+(%w+)\")\n+    end\n+\n+    if expand_group then\n+        return _build_tabgroup_menu_grouptabs(w, expand_group, field, order)\n+    else\n+        return _build_tabgroup_menu_grouplist(w, field, order)\n+    end\n+end\n+\n+local _operation = nil;\n+\n+switch_tabgroup = function  (w, group)\n+    if group ~= w2groups[w].active then\n+        local g = w2groups[w].groups[group]\n+        local nb = g._notebook\n+        local group_nb = assert(w.tabs.parent)\n+        group_nb:switch(group_nb:indexof(nb))\n+        w.tablist:set_notebook(nb)\n+        w.tabs = nb\n+\n+        -- changing name of active tabgroup and updating stats\n+        w2groups[w].active = group\n+        g.group_hit = g.group_hit + 1\n+        g.atime = os.time()\n+\n+        -- if we switching to empty tabgroup -- let's open new default tab\n+        if nb:count() == 0 then\n+            w:new_tab(settings.get_setting(\"window.new_tab_page\"), false)\n+        end\n+\n+        -- copy-paste from window.lua, since that handler is only attached to the initial notebook\n+        w.view = nil\n+        -- Update widgets after tab switch\n+        luakit.idle_add(function ()\n+            -- Cancel if window already destroyed\n+            if not w.win then return end\n+            if not w.view then return end\n+            w.view:emit_signal(\"switched-page\")\n+            w:update_win_title()\n+        end)\n+    end\n+end\n+\n+delete_tabgroup = function (w, group)\n+    assert(group)\n+    assert(type(group) == 'string')\n+    if number_of_tabgroups(w) == 1 then\n+        return nil\n+    else\n+        -- lets switch to another group\n+        if w2groups[w].active == group then\n+            switch_tabgroup(w, _select_next_opened_tabgroup(w, group))\n+        end\n+\n+        local g = w2groups[w].groups[group]\n+        w.tabs.parent:remove(g._notebook)\n+        table.insert(_deleted_groups, g)\n+        w2groups[w].groups[group] = nil\n+\n+        -- idle handler for cleaning removed notebooks\n+        luakit.idle_add(_cleaner)\n+    end\n+    return true\n+end\n+\n+-- View tabgroups in list and switch between then\n+local function new_tabgroup(w)\n+    w:set_mode('tabgroup-menu-new')\n+end\n+\n+local function rename_tabgroup(w)\n+    local row = w.menu:get()\n+    if row and row._group then\n+        w:set_mode('tabgroup-menu-rename')\n+    end\n+end\n+\n+-- needed for forward declaration\n+local show_tabgroup_content, show_tabgroups, move_tab_to_tabgroup_menu\n+\n+show_tabgroup_content = function (w, tabgroup_name)\n+    if tabgroup_name and type(tabgroup_name) == 'table' then\n+        tabgroup_name = nil\n+    end\n+    if not tabgroup_name then\n+        local row = w.menu:get()\n+        if row and row._group then\n+            tabgroup_name = row._group\n+        end\n+    end\n+    if tabgroup_name then\n+        local rows = build_tabgroup_menu(w, tabgroup_name)\n+        w.menu:build(rows)\n+        w.menu:update()\n+        local notify = _default_notify\n+        if number_of_tabgroups(w) > 1 then\n+            notify = notify ..\", \"..\"m: move selected tab to another tabgroup, -: show list of groups\"\n+        else\n+            notify = notify ..\", \"..\"-: show list of groups\"\n+        end\n+        w:notify(notify, false)\n+    end\n+end\n+\n+show_tabgroups = function (w)\n+    local rows = build_tabgroup_menu(w)\n+    w.menu:build(rows)\n+    w.menu:update()\n+    local notify = _default_notify ..\", \".. \"+: show tabs in selected group\"\n+    w:notify(notify, false)\n+end\n+\n+local function switch_tabgroup_or_tab(w, _, m)\n+    local row\n+    if m and m.count then\n+        row = w.menu:get(m.count+1)\n+    else\n+        row = w.menu:get()\n+    end\n+    if row and row._group then\n+        w:set_mode()\n+        switch_tabgroup(w, row._group);\n+        if row._tab and row._tab ~= current_webview_in_group(w, w2groups[w].active) then\n+            local idx = webview2idx(w, row._group, row._tab)\n+            if idx then\n+                w:goto_tab(idx)\n+            end\n+        end\n+    end\n+end\n+\n+local function open_tabgroup_menu(w, tabgroup_name)\n+    local expand = nil\n+    if tabgroup_name and type(tabgroup_name) == 'string' then\n+        expand = tabgroup_name\n+    end\n+    w:set_prompt()\n+    w:set_mode(\"tabgroup-menu\", expand)\n+end\n+\n+move_tab_to_tabgroup_menu = function (w)\n+    if number_of_tabgroups(w) > 1 then\n+        local row = w.menu:get()\n+        if row and row._group and row._tab then\n+            _operation = { op = 'move', _group = row._group, _tab = row._tab }\n+            w:set_mode('tabgroup-menu-select')\n+        end\n+    end\n+end\n+\n+new_mode(\"tabgroup-menu-new\", {\n+    enter = function (w)\n+        local groupname = _get_next_tabgroup_name(w)\n+        w:set_prompt(\"Enter name of new tabgroup > \")\n+        w:set_input(groupname)\n+    end,\n+\n+    activate = function (w, name)\n+        if not w2groups[w].groups[name] then\n+            create_tabgroup(w, name)\n+        else\n+            w:notify(\"Tabgroup '\"..name..\"' already exists\")\n+        end\n+        w:set_mode('tabgroup-menu')\n+    end,\n+})\n+\n+modes.add_binds(\"tabgroup-menu-new\", {\n+    { \"<Escape>\", \"Return to `tabgroup-menu` mode.\", open_tabgroup_menu },\n+})\n+\n+new_mode(\"tabgroup-menu-rename\", {\n+    enter = function (w)\n+        --_operation = nil\n+        local row = w.menu:get()\n+        local groupname = row._group\n+        w:set_prompt(\"Enter new name of tabgroup '\"..groupname..\"' > \")\n+        w:set_input(groupname)\n+        _operation = { op = 'rename', _group = groupname }\n+    end,\n+\n+    activate = function (w, new_name)\n+        local old_name = _operation._group\n+        if old_name ~= new_name then\n+            w2groups[w].groups[new_name] = w2groups[w].groups[old_name]\n+            w2groups[w].groups[old_name] = nil\n+            if w2groups[w].active == old_name then\n+                w2groups[w].active = new_name\n+            end\n+        end\n+        w:set_mode('tabgroup-menu')\n+    end,\n+})\n+\n+modes.add_binds(\"tabgroup-menu-rename\", {\n+    { \"<Escape>\", \"Return to `tabgroup-menu` mode.\", open_tabgroup_menu },\n+})\n+\n+local function move_tab_to_tabgroup(w, view, group)\n+    local old_group = webview2group(view)\n+    if old_group.name ~= group then\n+        local tg = create_tabgroup(w, group)\n+        local stats = w2groups[w].groups[old_group.name].tabs[view]\n+        w2groups[w].groups[old_group.name]._notebook:remove(view)\n+        tg._notebook:insert(view)\n+        tg.tabs[view] = { tab_hit = stats.tab_hit,\n+            ctime = stats.ctime,\n+            atime = stats.atime,\n+            mtime = stats.mtime,\n+        }\n+    end\n+end\n+\n+-- something that manages different operations when tabgroup select in tabgroup-menu-select mode\n+-- (so far only moving tabs between groups)\n+local _moving = false\n+local function select_tabgroup(w)\n+    local row = w.menu:get()\n+    if row and row._group then\n+        if _operation and _operation.op == 'move' and _operation._group and _operation._tab then\n+            if row._group ~= _operation._group then\n+                local tab = _operation._tab\n+                _moving = true\n+                w:set_mode()\n+                move_tab_to_tabgroup(w, tab, row._group)\n+                _moving = false\n+            else\n+                w:notify(\"Can't move tab to same tabgroup\")\n+            end\n+            w:set_mode('tabgroup-menu')\n+        end\n+    end\n+end\n+\n+new_mode(\"tabgroup-menu-select\", {\n+    enter = function (w)\n+        local rows = build_tabgroup_menu(w)\n+        w.menu:build(rows)\n+        w:notify(\"Press <Return> to select tabgroup\", false)\n+    end,\n+\n+    leave = function (w)\n+        if not _moving then\n+            w:set_mode('tabgroup-menu')\n+        end\n+    end,\n+})\n+\n+add_binds(\"tabgroup-menu-select\",  lousy.util.table.join({\n+    { \"<Return>\", \"Select tabgroup.\", select_tabgroup }\n+}, menu_binds))\n+\n+new_mode(\"tabgroup-menu\", {\n+    enter = function (w, tabgroup_name)\n+        local rows = build_tabgroup_menu(w, tabgroup_name)\n+        _operation = nil\n+        w.menu:build(rows)\n+        w:set_input()\n+        local notify = _default_notify\n+        if tabgroup_name then\n+            if number_of_tabgroups(w) > 1 then\n+                notify = notify ..\", \"..\"m: move selected tab to another tabgroup, -: show list of groups\"\n+            else\n+                notify = notify ..\", \"..\"-: show list of groups\"\n+            end\n+        else\n+            notify = notify ..\", \".. \"+: show tabs in selected group\"\n+        end\n+        w:notify(notify, false)\n+    end,\n+\n+    leave = function (w)\n+        w.menu:hide()\n+    end,\n+})\n+\n+add_binds(\"tabgroup-menu\", lousy.util.table.join({\n+    { \"<Return>\", \"Switch to tabgroup or tab in tabgroup.\", switch_tabgroup_or_tab },\n+    { \"n\", \"Create new tabgroup.\", new_tabgroup },\n+    { \"r\", \"Rename tabgroup.\", rename_tabgroup },\n+    { \"d\", \"Delete tabgroup.\", function(w)\n+        local row = w.menu:get()\n+        w:set_mode()\n+        if row and row._group then\n+            local deleted = delete_tabgroup(w, row._group)\n+            open_tabgroup_menu(w)\n+            if not deleted then\n+                w:notify(\"Can't remove last tabgroup\")\n+            end\n+        end\n+    end},\n+    { \"+\", \"Open list of tabs in tabgroup.\", show_tabgroup_content },\n+    { \"-\", \"Hide list of tabs in tabgroup.\", show_tabgroups },\n+    { \"m\", \"Move tab to different tabgroup.\", move_tab_to_tabgroup_menu },\n+    { \"1\", \"Switch to first tab or tabgroup.\", switch_tabgroup_or_tab, { count = 1 } },\n+    { \"2\", \"Switch to second tab or tabgroup.\", switch_tabgroup_or_tab, { count = 2 }  },\n+    { \"3\", \"Switch to third tab or tabgroup.\", switch_tabgroup_or_tab, { count = 3 }  },\n+    { \"4\", \"Switch to forth tab or tabgroup.\", switch_tabgroup_or_tab, { count = 4 }  },\n+    { \"5\", \"Switch to fifth tab or tabgroup.\", switch_tabgroup_or_tab, { count = 5 }  },\n+    { \"6\", \"Switch to sixth tab or tabgroup.\", switch_tabgroup_or_tab, { count = 6 }  },\n+    { \"7\", \"Switch to seventh tab or tabgroup.\", switch_tabgroup_or_tab, { count = 7 }  },\n+    { \"8\", \"Switch to eighth tab or tabgroup.\", switch_tabgroup_or_tab, { count = 8 }  },\n+    { \"9\", \"Switch to ninth tab or tabgroup.\", switch_tabgroup_or_tab, { count = 9 }  },\n+}, menu_binds))\n+\n+add_binds(\"normal\", {\n+    { \"x\", \"Open tabgroup menu.\", open_tabgroup_menu },\n+    { \"X\", \"Show tabs in active tabgroup.\", function (w) open_tabgroup_menu(w, w2groups[w].active)  end },\n+})\n+\n+settings.register_settings({\n+    [\"tabgroups.sort_groups_by\"] = {\n+        type = \"string\",\n+        default = \"name asc\",\n+        validator = function(v)\n+            local field, order = string.match(v, \"^%s*(%w+)%s+(%w+)\")\n+            if field then\n+                field = string.lower(field)\n+                if field ~= 'name' and","path":"tabgroups/init.lua","position":777,"original_position":777,"commit_id":"95add8fccf7e0cd66590b7da6cd2ab97bccad263","original_commit_id":"95add8fccf7e0cd66590b7da6cd2ab97bccad263","user":{"login":"aidanholm","id":10691965,"avatar_url":"https://avatars3.githubusercontent.com/u/10691965?v=4","gravatar_id":"","url":"https://api.github.com/users/aidanholm","html_url":"https://github.com/aidanholm","followers_url":"https://api.github.com/users/aidanholm/followers","following_url":"https://api.github.com/users/aidanholm/following{/other_user}","gists_url":"https://api.github.com/users/aidanholm/gists{/gist_id}","starred_url":"https://api.github.com/users/aidanholm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aidanholm/subscriptions","organizations_url":"https://api.github.com/users/aidanholm/orgs","repos_url":"https://api.github.com/users/aidanholm/repos","events_url":"https://api.github.com/users/aidanholm/events{/privacy}","received_events_url":"https://api.github.com/users/aidanholm/received_events","type":"User","site_admin":false},"body":"- `field` and `order` are either both set or both nil\r\n- if they're both nil, that's an error condition: return false\r\n```lua\r\nif not field then return false end\r\nfield, order = field:lower(), order:lower()\r\nfield = ({name = true, atime = true, mtime = true, ctime = true, hits = true})[field]\r\norder = order == \"asc\" or order == \"desc\"\r\nreturn field and order\r\n```","created_at":"2017-12-13T13:24:35Z","updated_at":"2017-12-13T13:30:01Z","html_url":"https://github.com/luakit/luakit-plugins/pull/19#discussion_r156656702","pull_request_url":"https://api.github.com/repos/luakit/luakit-plugins/pulls/19","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/luakit/luakit-plugins/pulls/comments/156656702"},"html":{"href":"https://github.com/luakit/luakit-plugins/pull/19#discussion_r156656702"},"pull_request":{"href":"https://api.github.com/repos/luakit/luakit-plugins/pulls/19"}}},"pull_request":{"url":"https://api.github.com/repos/luakit/luakit-plugins/pulls/19","id":158081283,"html_url":"https://github.com/luakit/luakit-plugins/pull/19","diff_url":"https://github.com/luakit/luakit-plugins/pull/19.diff","patch_url":"https://github.com/luakit/luakit-plugins/pull/19.patch","issue_url":"https://api.github.com/repos/luakit/luakit-plugins/issues/19","number":19,"state":"open","locked":false,"title":"implemented tabgroups plugin","user":{"login":"serg-kozhemyakin","id":11452776,"avatar_url":"https://avatars1.githubusercontent.com/u/11452776?v=4","gravatar_id":"","url":"https://api.github.com/users/serg-kozhemyakin","html_url":"https://github.com/serg-kozhemyakin","followers_url":"https://api.github.com/users/serg-kozhemyakin/followers","following_url":"https://api.github.com/users/serg-kozhemyakin/following{/other_user}","gists_url":"https://api.github.com/users/serg-kozhemyakin/gists{/gist_id}","starred_url":"https://api.github.com/users/serg-kozhemyakin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serg-kozhemyakin/subscriptions","organizations_url":"https://api.github.com/users/serg-kozhemyakin/orgs","repos_url":"https://api.github.com/users/serg-kozhemyakin/repos","events_url":"https://api.github.com/users/serg-kozhemyakin/events{/privacy}","received_events_url":"https://api.github.com/users/serg-kozhemyakin/received_events","type":"User","site_admin":false},"body":"@aidanholm can you review this pls? it's same as before just with some lines wrapped","created_at":"2017-12-13T12:32:09Z","updated_at":"2017-12-13T13:30:01Z","closed_at":null,"merged_at":null,"merge_commit_sha":"12fd34f567acad283c63fb93c869d002d19a2a2d","assignee":null,"assignees":[],"requested_reviewers":[],"milestone":null,"commits_url":"https://api.github.com/repos/luakit/luakit-plugins/pulls/19/commits","review_comments_url":"https://api.github.com/repos/luakit/luakit-plugins/pulls/19/comments","review_comment_url":"https://api.github.com/repos/luakit/luakit-plugins/pulls/comments{/number}","comments_url":"https://api.github.com/repos/luakit/luakit-plugins/issues/19/comments","statuses_url":"https://api.github.com/repos/luakit/luakit-plugins/statuses/95add8fccf7e0cd66590b7da6cd2ab97bccad263","head":{"label":"serg-kozhemyakin:candidates","ref":"candidates","sha":"95add8fccf7e0cd66590b7da6cd2ab97bccad263","user":{"login":"serg-kozhemyakin","id":11452776,"avatar_url":"https://avatars1.githubusercontent.com/u/11452776?v=4","gravatar_id":"","url":"https://api.github.com/users/serg-kozhemyakin","html_url":"https://github.com/serg-kozhemyakin","followers_url":"https://api.github.com/users/serg-kozhemyakin/followers","following_url":"https://api.github.com/users/serg-kozhemyakin/following{/other_user}","gists_url":"https://api.github.com/users/serg-kozhemyakin/gists{/gist_id}","starred_url":"https://api.github.com/users/serg-kozhemyakin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serg-kozhemyakin/subscriptions","organizations_url":"https://api.github.com/users/serg-kozhemyakin/orgs","repos_url":"https://api.github.com/users/serg-kozhemyakin/repos","events_url":"https://api.github.com/users/serg-kozhemyakin/events{/privacy}","received_events_url":"https://api.github.com/users/serg-kozhemyakin/received_events","type":"User","site_admin":false},"repo":{"id":114117443,"name":"luakit-plugins","full_name":"serg-kozhemyakin/luakit-plugins","owner":{"login":"serg-kozhemyakin","id":11452776,"avatar_url":"https://avatars1.githubusercontent.com/u/11452776?v=4","gravatar_id":"","url":"https://api.github.com/users/serg-kozhemyakin","html_url":"https://github.com/serg-kozhemyakin","followers_url":"https://api.github.com/users/serg-kozhemyakin/followers","following_url":"https://api.github.com/users/serg-kozhemyakin/following{/other_user}","gists_url":"https://api.github.com/users/serg-kozhemyakin/gists{/gist_id}","starred_url":"https://api.github.com/users/serg-kozhemyakin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serg-kozhemyakin/subscriptions","organizations_url":"https://api.github.com/users/serg-kozhemyakin/orgs","repos_url":"https://api.github.com/users/serg-kozhemyakin/repos","events_url":"https://api.github.com/users/serg-kozhemyakin/events{/privacy}","received_events_url":"https://api.github.com/users/serg-kozhemyakin/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/serg-kozhemyakin/luakit-plugins","description":"Version control for various luakit plugins.","fork":true,"url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins","forks_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/forks","keys_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/keys{/key_id}","collaborators_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/teams","hooks_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/hooks","issue_events_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/issues/events{/number}","events_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/events","assignees_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/assignees{/user}","branches_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/branches{/branch}","tags_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/tags","blobs_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/git/refs{/sha}","trees_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/git/trees{/sha}","statuses_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/statuses/{sha}","languages_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/languages","stargazers_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/stargazers","contributors_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/contributors","subscribers_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/subscribers","subscription_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/subscription","commits_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/commits{/sha}","git_commits_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/git/commits{/sha}","comments_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/comments{/number}","issue_comment_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/issues/comments{/number}","contents_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/contents/{+path}","compare_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/compare/{base}...{head}","merges_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/merges","archive_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/downloads","issues_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/issues{/number}","pulls_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/pulls{/number}","milestones_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/milestones{/number}","notifications_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/labels{/name}","releases_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/releases{/id}","deployments_url":"https://api.github.com/repos/serg-kozhemyakin/luakit-plugins/deployments","created_at":"2017-12-13T12:22:10Z","updated_at":"2017-12-13T12:22:12Z","pushed_at":"2017-12-13T12:26:43Z","git_url":"git://github.com/serg-kozhemyakin/luakit-plugins.git","ssh_url":"git@github.com:serg-kozhemyakin/luakit-plugins.git","clone_url":"https://github.com/serg-kozhemyakin/luakit-plugins.git","svn_url":"https://github.com/serg-kozhemyakin/luakit-plugins","homepage":null,"size":62,"stargazers_count":0,"watchers_count":0,"language":"Lua","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"luakit:master","ref":"master","sha":"e88d2403fa6c1777e8590fabb19e46160dad2b2e","user":{"login":"luakit","id":19660163,"avatar_url":"https://avatars2.githubusercontent.com/u/19660163?v=4","gravatar_id":"","url":"https://api.github.com/users/luakit","html_url":"https://github.com/luakit","followers_url":"https://api.github.com/users/luakit/followers","following_url":"https://api.github.com/users/luakit/following{/other_user}","gists_url":"https://api.github.com/users/luakit/gists{/gist_id}","starred_url":"https://api.github.com/users/luakit/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/luakit/subscriptions","organizations_url":"https://api.github.com/users/luakit/orgs","repos_url":"https://api.github.com/users/luakit/repos","events_url":"https://api.github.com/users/luakit/events{/privacy}","received_events_url":"https://api.github.com/users/luakit/received_events","type":"Organization","site_admin":false},"repo":{"id":4833143,"name":"luakit-plugins","full_name":"luakit/luakit-plugins","owner":{"login":"luakit","id":19660163,"avatar_url":"https://avatars2.githubusercontent.com/u/19660163?v=4","gravatar_id":"","url":"https://api.github.com/users/luakit","html_url":"https://github.com/luakit","followers_url":"https://api.github.com/users/luakit/followers","following_url":"https://api.github.com/users/luakit/following{/other_user}","gists_url":"https://api.github.com/users/luakit/gists{/gist_id}","starred_url":"https://api.github.com/users/luakit/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/luakit/subscriptions","organizations_url":"https://api.github.com/users/luakit/orgs","repos_url":"https://api.github.com/users/luakit/repos","events_url":"https://api.github.com/users/luakit/events{/privacy}","received_events_url":"https://api.github.com/users/luakit/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/luakit/luakit-plugins","description":"Version control for various luakit plugins.","fork":false,"url":"https://api.github.com/repos/luakit/luakit-plugins","forks_url":"https://api.github.com/repos/luakit/luakit-plugins/forks","keys_url":"https://api.github.com/repos/luakit/luakit-plugins/keys{/key_id}","collaborators_url":"https://api.github.com/repos/luakit/luakit-plugins/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/luakit/luakit-plugins/teams","hooks_url":"https://api.github.com/repos/luakit/luakit-plugins/hooks","issue_events_url":"https://api.github.com/repos/luakit/luakit-plugins/issues/events{/number}","events_url":"https://api.github.com/repos/luakit/luakit-plugins/events","assignees_url":"https://api.github.com/repos/luakit/luakit-plugins/assignees{/user}","branches_url":"https://api.github.com/repos/luakit/luakit-plugins/branches{/branch}","tags_url":"https://api.github.com/repos/luakit/luakit-plugins/tags","blobs_url":"https://api.github.com/repos/luakit/luakit-plugins/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/luakit/luakit-plugins/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/luakit/luakit-plugins/git/refs{/sha}","trees_url":"https://api.github.com/repos/luakit/luakit-plugins/git/trees{/sha}","statuses_url":"https://api.github.com/repos/luakit/luakit-plugins/statuses/{sha}","languages_url":"https://api.github.com/repos/luakit/luakit-plugins/languages","stargazers_url":"https://api.github.com/repos/luakit/luakit-plugins/stargazers","contributors_url":"https://api.github.com/repos/luakit/luakit-plugins/contributors","subscribers_url":"https://api.github.com/repos/luakit/luakit-plugins/subscribers","subscription_url":"https://api.github.com/repos/luakit/luakit-plugins/subscription","commits_url":"https://api.github.com/repos/luakit/luakit-plugins/commits{/sha}","git_commits_url":"https://api.github.com/repos/luakit/luakit-plugins/git/commits{/sha}","comments_url":"https://api.github.com/repos/luakit/luakit-plugins/comments{/number}","issue_comment_url":"https://api.github.com/repos/luakit/luakit-plugins/issues/comments{/number}","contents_url":"https://api.github.com/repos/luakit/luakit-plugins/contents/{+path}","compare_url":"https://api.github.com/repos/luakit/luakit-plugins/compare/{base}...{head}","merges_url":"https://api.github.com/repos/luakit/luakit-plugins/merges","archive_url":"https://api.github.com/repos/luakit/luakit-plugins/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/luakit/luakit-plugins/downloads","issues_url":"https://api.github.com/repos/luakit/luakit-plugins/issues{/number}","pulls_url":"https://api.github.com/repos/luakit/luakit-plugins/pulls{/number}","milestones_url":"https://api.github.com/repos/luakit/luakit-plugins/milestones{/number}","notifications_url":"https://api.github.com/repos/luakit/luakit-plugins/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/luakit/luakit-plugins/labels{/name}","releases_url":"https://api.github.com/repos/luakit/luakit-plugins/releases{/id}","deployments_url":"https://api.github.com/repos/luakit/luakit-plugins/deployments","created_at":"2012-06-29T14:21:55Z","updated_at":"2017-11-10T00:44:02Z","pushed_at":"2017-12-13T12:32:10Z","git_url":"git://github.com/luakit/luakit-plugins.git","ssh_url":"git@github.com:luakit/luakit-plugins.git","clone_url":"https://github.com/luakit/luakit-plugins.git","svn_url":"https://github.com/luakit/luakit-plugins","homepage":null,"size":56,"stargazers_count":42,"watchers_count":42,"language":"Lua","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":15,"mirror_url":null,"archived":false,"open_issues_count":6,"license":{"key":"gpl-3.0","name":"GNU General Public License v3.0","spdx_id":"GPL-3.0","url":"https://api.github.com/licenses/gpl-3.0"},"forks":15,"open_issues":6,"watchers":42,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/luakit/luakit-plugins/pulls/19"},"html":{"href":"https://github.com/luakit/luakit-plugins/pull/19"},"issue":{"href":"https://api.github.com/repos/luakit/luakit-plugins/issues/19"},"comments":{"href":"https://api.github.com/repos/luakit/luakit-plugins/issues/19/comments"},"review_comments":{"href":"https://api.github.com/repos/luakit/luakit-plugins/pulls/19/comments"},"review_comment":{"href":"https://api.github.com/repos/luakit/luakit-plugins/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/luakit/luakit-plugins/pulls/19/commits"},"statuses":{"href":"https://api.github.com/repos/luakit/luakit-plugins/statuses/95add8fccf7e0cd66590b7da6cd2ab97bccad263"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"aidanholm"}}