Управлять сотнями вкладок в Chrome с помощью JXA и Alfred

Перевод материала Ринана Кейкирерка, бэкенд-разработчика Uber.

Управлять сотнями вкладок в Chrome с помощью JXA и Alfred

Несколько сотен открытых вкладок уже долгое время являются для меня проблемой. Ну, может и не несколько сотен, но точно около сотни. Чем больше у меня открытых вкладок, тем больше среди них повторяющих друг друга. В итоге я не пытаюсь их разобрать, а просто закрываю браузер и начинаю сначала, теряя в результате что-то важное или интересное.

Частично в этом можно обвинить плохую и устаревшую структуру интерфейса современных браузеров. Вообще, всё, чем занимаются их разработчики с позиции структуры и дизайна интерфейса — это делают браузеры чуть привлекательнее внешне.

Например, просто вбивать ключевые слова в адресную строку в поисках ссылки, по которой вы когда-то переходили — очень плохой способ её найти. В Chrome, кажется, есть некий поиск по неточному соответствию, но работает он плохо, в отличие от прекрасного инструмента fzf.

Поиск по неточному соответствию с помощью fzf в iTerm2

Ещё одним примером может служить работа с несколькими вкладками. В Chrome нет поиска по вкладкам, располагать их вертикально нельзя, а если вкладок слишком много, то в горизонтальном положении видны только иконки. К тому же, нельзя избавиться от повторяющихся вкладок.

Да, для этого можно скачать расширение. Пусть это прозвучит параноидально, но я не хочу пользоваться никакими расширениями, которые запрашивают такой доступ:

  • Читать и изменять данные на посещаемых сайтах.

  • Изменять стартовую страницу при открытии новой вкладки.

  • Просматривать и изменять историю браузера.
  • Отображать оповещения.
Безумный список разрешений, которые сейчас запрашивают расширения для Chrome
Безумный список разрешений, которые сейчас запрашивают расширения для Chrome

Да, это означает, что я не пользуюсь никакими расширениями для Chrome, несмотря на их очевидные преимущества. Я знаю, что некоторые из них созданы на основе открытого программного кода, но открытый код не гарантирует безопасности, особенно если проверить весь код лень.

Итак, я решил сам разобраться с проблемой управления вкладками, потому что для творчества время всегда найдётся.

Выбор подходящих инструментов

Первым пунктом в списке стояло найти способ управлять Chrome. Я рассматривал следующие варианты:

  • Написать расширение для Chrome самому: делать этого мне не хотелось, потому что расширения полезны только тогда, когда большая часть работы происходит в браузере. То есть, когда я работаю в другом окне, например, терминале, а потом хочу открыть определённую вкладку, мне придётся открыть Chrome, а потом включить расширение. Слишком много действий.
  • Использовать AppleScript: AppleScript — это язык сценариев для контроля приложений с помощью Apple Events. Этот вариант мне понравился, пока я не попробовал разные примеры. Мне было очень неудобно, поскольку я всю жизнь пользовался C, C++, Python, Java, Go и JavaScript. Так что я решил вернуться к этому, только если не найду ничего получше.
  • Использовать Python: Я обрадовался, узнав, что есть библиотеки, которые можно использовать для работы с macOS. В итоге оказалось, что те давно устарели и никакой документации по ним не было. От этого варианта тоже пришлось отказаться, потому что мне необходимо было быстрое решение.

Но я не сдавался, продолжал изучать пути решения и наткнулся на JXA.

Что такое JXA

JXA — это «JavaScript для Автоматизации». Он поддерживается Apple, позволяет управлять приложениями с помощью AppleScript, поддерживает синтаксис ES6; в целом всё это звучит слишком хорошо, чтобы быть правдой… Так и оказалось, потому что у него худшая документация, которую я когда-либо видел у Apple.

Я всё ещё не знаю, что в аббревиатуре JXA означает X. Может, macOS X? ¯\_(ツ)_/¯

Я посмотрел забавные заметки о выпуске (документация JXA от Apple…), а затем подумал: «Так, JavaScript, неужели трудно собраться и самому разобраться?»

Я и представить не мог, что меня ждёт.

Несколько часов я потратил на поиски места для написания кода, расширения имени файла, выполнения файла и прикладного программного интерфейса.

Но в конце концов, это того стоило.

Начало работы с JXA

В основном, писать JXA рекомендуют в Script Editor App или Automator App, который идёт в комплекте с macOS. Я попробовал их, чуть не выкинул ноутбук в окно и решил использовать редактор, которым пользуюсь всегда: VSCode.

Я был пользователем Vim в течение примерно четырех лет, в течение года я ипользовал IDE JetBrains, Atom и Sublime, потом перешел на VSCode, и после четырех лет ежедневного использования в основном с Go, а иногда с Python, JavaScript и Markdown я до сих пор думаю, что это лучший редактор; и каждый должен попробовать пользоваться им месяц-другой, прежде чем судить.

Вот что стоит знать, прежде чем взяться за работу:

  • Создайте файл с обычным расширением js (например, script.js)
  • Вставьте эту строку первой в файле: #!/usr/bin/env osascript -l JavaScript
  • Сделайте сценарий исполняемым: chmod +x ./script.js
  • И запустите его с помощью: ./script.js

Можете воспользоваться этим шаблоном:

console.log("Hello multiverse 👽")

Подключение к Chrome

Самой сложной проблемой оказалось придумать, как подключаться к приложениям. Для методов нет автозавершения, а вывод полей и методов объекта не сработал.

Затем я случайно наткнулся на статью, в которой упоминается использование функции редактора сценариев: «Открыть словарь».

Открытие словаря в редакторе сценариев
Открытие словаря в редакторе сценариев

И вуаля! Именно то, что я искал: небольшая заметка для каждого приложения, в которой перечислены методы и объекты; и там был Chrome!

Открытие словаря Google Chrome в редакторе сценариев
Открытие словаря Google Chrome в редакторе сценариев

Вот как выглядит интерфейс.

Словарь Google Chrome в редакторе сценариев
Словарь Google Chrome в редакторе сценариев

Не лучший вариант, но работать с ним можно.

Итак, первое, что необходимо было сделать, это создать копию Chrome:

const chrome = Application('Google Chrome') chrome.includeStandardAdditions = true

includeStandardAdditions используется для добавления стандартных методов, например, displayDialog в копию приложения.

Понять, как взаимодействовать с этой копией, оказалось довольно просто. Вот как создать список всех вкладок во всех окнах:

chrome.windows().forEach((window, winIdx) => { window.tabs().forEach((tab, tabIdx) => { console.log(tab.title(), tab.url()) }) })

Закрыть вкладки тоже оказалось довольно просто:

chrome.windows[winIdx].tabs[tabIdx].close()

Остановиться на определённой вкладке в определённом окне немного сложнее:

chrome.windows[winIdx].visible = true chrome.windows[winIdx].activeTabIndex = tabIdx chrome.windows[winIdx].index = 1 chrome.activate()

Затем я хотел вывести текст в формате JSON, чтобы передать его на спасительную команду jq, но тут понял, что console.log выводит данные в stderr, а функции для вывода в stdout не существует, а я не хотел перенаправлять stderr на stdout.

К счастью, я узнал, что можно импортировать Objective C в JXA. И решил написать свою функцию для вывода:

ObjC.import('stdlib') ObjC.import('Foundation') const print = function (msg) { $.NSFileHandle.fileHandleWithStandardOutput.writeData( $.NSString.alloc.initWithString(String(msg)) .dataUsingEncoding($.NSUTF8StringEncoding) ) }

Хоть понимание того, как это работает, и заняло много времени, оно оказалось очень полезным. К счастью, у меня уже был небольшой опыт работы с Objective C, который помог в проекте «Как можно смотреть трейлеры фильмов на постерах к ним с помощью iPad и дополненной реальности» в 2011–2012 годах, когда AR еще только начинала развиваться на iOS, и я возлагал на неё большие надежды…

После этого, чтобы получить входную информацию от терминала, я должен был написать свою собственную входную функцию:

const input = function (msg) { print(msg) return $.NSString.alloc.initWithDataEncoding( $.NSFileHandle.fileHandleWithStandardInput.availableData, $.NSUTF8StringEncoding ).js.trim() }

Наконец, мне надо было найти способ отобразить подсказку Chrome, которая будет спрашивать, действительно ли я хочу закрыть перечисленные вкладки:

chrome.displayDialog(msg)

Появление нового проекта: Chrome Control

Я собрал всё в единый проект, который назвал Chrome Control. Может быть, кто-то сможет использовать его вместе со своим любимым инструментом — например, vim или fzf. Программный код лежит на GitHub.

Вот как выглядит Chrome Control:

Chrome Control в iTerm2
Chrome Control в iTerm2

Теперь, когда у меня появилось решение, пришло время использовать Chrome Control в лучшем приложении для продуктивности в нашей мультивселенной: Alfred.

Я считаю, Apple стоит отказаться от устаревшего Spotlight и задуматься о приобретении Alfred.

Использование Chrome Control с Alfred

Alfred — одно из лучших приложений, благодаря которому я — 🦄без преувеличения — становлюсь в десять раз продуктивнее. Я создал десятки рабочих процессов, которые ускоряют выполнение повседневных задач.

С этим проектом моей мечтой стало иметь возможность нажать Alt + T в любом месте macOS, начать вводить название вкладки и мгновенно увидеть список вкладок, отфильтрованных с помощью поиска по неточным соответствиям, затем просто выделить нужную вкладку, нажать Enter и перейти к ней.

Для начала мне нужно было создать новый рабочий процесс в Alfred.

Создание нового рабочего процесса
Создание нового рабочего процесса

Я сделал так, чтобы Chrome Control выводил список вкладок в формате JSON для включения интеграции. Это помогает, потому что, если бы я хотел получить список вкладок в Alfred, мне пришлось бы использовать фильтр сценариев, а фильтр сценариев требует вывода в формате JSON с дополнительными особыми полями.

Вот выходные данные команды list:

{ "items": [ { "title": "Inbox (1) - <hidden>@gmail.com - Gmail", "url": "https://mail.google.com/mail/u/0/#inbox", "winIdx": 0, "tabIdx": 0, "arg": "0,0", "subtitle": "https://mail.google.com/mail/u/0/#inbox" }, { "title": "iPhone - Apple", "url": "https://www.apple.com/iphone/", "winIdx": 0, "tabIdx": 1, "arg": "0,1", "subtitle": "https://www.apple.com/iphone/" } ] }

Поля arg и subtitle необходимы для фильтра сценариев, ниже мы рассмотрим это подробнее.

Добавление нового фильтра сценариев в рабочий процесс
Добавление нового фильтра сценариев в рабочий процесс

Создание фильтра сценариев открывает такое окно:

Создание фильтра сценариев
Создание фильтра сценариев

Я хотел, чтобы ключевым словом было tabs, чтобы каждый раз, когда я набираю tabs, появлялся список всех вкладок во всех окнах.

Стоит обратить внимание на множество других вещей здесь. Во-первых, with space argument optional. Это говорит Alfred ожидать только команды tabs или дополнительного аргумента, например, tabs apple, который отобразит все связанные с Apple вкладки.

У Alfred есть замечательная функция поиска неточных соответствий. Чтобы включить её, я просто установил флажок в Alfred filters results.

В разделе сценариев я сказал Alfred выполнить мою команду list. И перетащил иконку, созданную в Photoshop.

Вот как выглядит конечный результат:

Chrome Control на Alfred

Второй шаг — привязка сочетания Alt + T к этой команде. В Alfred сделать это очень просто при помощи триггера hotkey:

Создание триггера Hotkey
Создание триггера Hotkey

Установить его на Alt + T:

Установка триггера Hotkey
Установка триггера Hotkey

Затем я соединил его с фильтром сценариев с помощью связки:

Соединение триггера Hotkey с фильтром сценариев
Соединение триггера Hotkey с фильтром сценариев

Теперь, когда у меня появился способ использовать горячую клавишу и отфильтровать вкладку, можно выбрать нужную вкладку нажатием клавиши Enter.

Работа с необходимой вкладкой

С этой задачей пришлось повозиться. Мне надо дать Alfred команду запуска другого сценария по результату фильтра сценариев.

Помните значение arg, которое я выводил в JSON, с результатом 0,1? Первое значение — это Window Index, а второе — Tab Index. Мне нужно было передать это значение arg команде ./chrome.js focus.

К счастью, Alfred использует это значение arg, чтобы передать его как переменную шаблона {query} для дальнейших действий, которые вы подключаете к фильтру сценариев.

Это означает, что я могу подключить выходные данные фильтра сценариев к новому действию Run Script и передать значение arga команде focus.

Соединение выходных данных фильтра сценариев с действием Run Script
Соединение выходных данных фильтра сценариев с действием Run Script

А затем просто запускать команду ./chrome.js focus {query} всякий раз, когда элемент выбирается из списка.

Настройка фильтра сценариев для запуска команды focus при выборе элемента
Настройка фильтра сценариев для запуска команды focus при выборе элемента

Получилось!

Я также хотел иметь способ закрыть выделенные вкладки.Для этого я мог бы использовать клавишуAlt, которая является клавишей-модификатором в macOS.

Если вы не знали об этой функции, попробуйте щелкнуть значок Wi-Fi или значок динамика в строке меню, удерживая клавишу Alt, и вам откроются некоторые дополнительные функции.

Чтобы сделать это, мне нужно было подключить фильтр сценариев к моей команде close.

Присоедниение команды tabs к команде закрытия Chrome Control
Присоедниение команды tabs к команде закрытия Chrome Control

Чтобы сообщить Alfred, что этот сценарий должен запускаться только при нажатии клавиши Alt, вам нужно щелкнуть правой кнопкой мыши на соединение и настроить его.

Открытие настроек соединения
Открытие настроек соединения
Настройка действия клавиши Alt
Настройка действия клавиши Alt

Я хотел, чтобы текст «Закрыть эту вкладку» появлялся при удержании клавиши Alt. Результат выглядит следующим образом:

Удержание Alt и нажатие Enter закрывает вкладку
Удержание Alt и нажатие Enter закрывает вкладку

После этого мне захотелось добавить ещё больше функций, и я решил придумать решение для моей проблемы повторяющихся вкладок.

Удаление повторяющихся вкладок

Дублирующие друг друга вкладки давно были проблемой, для решения которой я решил добавить команду дедупликации в Chrome Control.

Она просто перебирает все открытые вкладки, находит дубликаты и закрывает их.

Но была одна загвоздка: что, если бы я случайно закрыл вкладку с несохраненной работой, частично заполненной формой, несохраненным документом или наполовину написанным письмом?

По этой причине я хотел, чтобы выскакивало окошко со списком вкладок, которые программа хотела закрыть, и спрашивало у меня, действительно ли я хочу их закрыть. Я сделал это параметром по умолчанию и добавил флажок --yes, чтобы иметь возможность принудительно закрыть все вкладки без появления окошка.

Вот пример избавления от одинаковых пяти вкладок Hacker News:

Удаление повторяющихся вкладок с помощью Chrome Control
Удаление повторяющихся вкладок с помощью Chrome Control

Затем я подсоединил это к Alfred. Но потом я понял, что теперь мне нужно уведомление в браузере. Поэтому я добавил флажок --ui. Когда этот флажок отмечен, Chrome Control будет показывать уведомления в браузере, а не в терминале.

Соединение команды dedup с Chrome Control
Соединение команды dedup с Chrome Control

Я просто создал keyword trigger, который соединил с ./chrome.js dedup. Теперь в Chrome появляется диалоговое окно!

Chrome спрашивает у меня, действительно ли я хочу закрыть одинаковые вкладки
Chrome спрашивает у меня, действительно ли я хочу закрыть одинаковые вкладки

Вот почему мы добавили следующую строку в начале нашего сценария. Она позволила этому окошку появиться.

chrome.includeStandardAdditions = true

Закрытие вкладок по ключевым словам

Ещё одна функция, которую я хотел добавить — это возможность закрывать вкладки по ключевым словам. Ключевые слова могут быть как в URL, так и в заголовке вкладки.

Например, если бы у меня было открыто несколько миллионов документов в Google Docs, я мог бы просто напечатать ./chrome.js close --url docs.google. И тогда Chrome Control найдет все URL-адреса, содержащие эту строку, и закроет вкладки.

Но я также хотел, чтобы это работало с заголовком вкладки. Например, если бы я изучал информацию о последнем iPhone, возможно, я мог бы закрыть все заголовки, которые включали слово iPhone следующим образом: ./chrome.js close --title iphone.

Итак, я решил создать обе эти команды.

Закрытие вкладок по названию
Закрытие вкладок по названию
Закрытие вкладок по URL
Закрытие вкладок по URL

Эти команды могут включать в себя несколько ключевых слов, разделенных пробелом. А если в самой фразе есть пробел, то её можно заключить в двойные кавычки, например, "это фраза с пробелами".

Затем я подсоединил их к Alfred, только в этот раз необходимо было создать аргумент.

Соединение команды закрытия вкладки с Alfred
Соединение команды закрытия вкладки с Alfred

А затем добавил действие Run Script и на этот раз использовал with input as argv. Это позволило мне использовать $@, которая отправляет все набранные ключевые слова в Chrome Control в качестве аргументов.

Установка команды закрытия url на Alfred
Установка команды закрытия url на Alfred
<p>Соединение ключевого слова close url с Chrome Control</p>

Соединение ключевого слова close url с Chrome Control

Вот как я закрываю все вкладки, содержащие apple или doc.

Закрытие вкладок, в которых содержатся слова apple или doc
Закрытие вкладок, в которых содержатся слова apple или doc

Я проделал то же самое для создания команды закрытия по названию на Alfred.

Я также добавил дополнительные горячие клавиши для запуска других команд:

  • Alt + T — показать все вкладки.
  • Alt + D — удалить повторяющиеся вкладки.
  • Alt + C — закрыть вкладки по URL.
  • Alt + Shift + C — закрыть вкладки по названию.

И вот как выглядит итоговый рабочий процесс:

Рабочий процесс Chrome Control на Alfred
Рабочий процесс Chrome Control на Alfred

Исходный код и рабочий процесс Chrome Control Alfred

Вы можете найти дополнительную документацию о Chrome Control и весь исходный код на GitHub.

Если вам кажется, что вам пригодится рабочий процесс Chrome Control, не стесняйтесь скачивать его с GitHub здесь.

Alfred не позволяет рабочим процессам экспортировать горячие клавиши для защиты пользователей от конфликтующих ярлыков. После импорта вам придется вручную устанавливать горячие клавиши, причем это должно занять не более 60 секунд.

Заключение

JXA в сочетании с Alfred — чрезвычайно мощный инструмент для создания практически неограниченного количества полезных рабочих процессов. Было очень сложно начать, найти хорошую документацию, но в итоге я смог создать то, о чём мечтал, и радости моей нет предела.

Конечно, было бы замечательно, если бы Chrome предоставлял такие функции по умолчанию, но я не очень оптимистично настроен по этому поводу.

1010
6 комментариев

Таких костылей, я, однако, никогда не встречал:)

8

Комментарий недоступен

4

Интересно, спасибо. :)

Я недавно тоже боролся со временными вкладками.
У меня их не было 100, но это были вкладки, которые не охота добалвять в закладки (т. к. их через пару дней из закладок нужно будет удалять вручную), а когда они "висели" незакрытыми, ожидая своей очереди, мешали концентрироваться на текущих задачах.

В итоге я пошел по пути раширения. Если интересно, вот что вышло:
https://chrome.google.com/webstore/detail/tab-bucket/ojmpjnfpigebajjcoddpjoihgjlahnpa

Эээ, но ведь есть же популярное (и, главное, удобное) расширение OneTab.
https://chrome.google.com/webstore/detail/onetab/chphlpgkkbolifaimnlloiipkdnihall

Гиковская херь.