Управление awesome-wm вне конфигурационного файла

в 11:08, , рубрики: awesome, linux, Lua, scala, велосипед, костыль, метки: , , , , ,

Привет. Во время использования awesome-wm (легкий тайлинговый оконный менеджер), даже когда всё нужное, казалось бы, уже настроено, всё равно замечаешь, что чего-то таки не хватает. Не может радовать то, что в большинстве случаев готового решения нету. Cлава б-гу, разработчики позаботились о хорошей документации и прокомментированному коду, что позволяет в короткие сроки дописать то, что тебе нужно. Горячие клавиши, управление окнами, собственные виджеты, даже создание собственных layout'ов — всё это делается проще, чем может сперва показаться. Но в ситуации с виджетами, как оказалось, не всё так гладко, как хотелось бы. Можно заметить, что добавив пару-тройку, обновляющихся посредством сети, либо просто занимающих сравнительно большое время на обновление информации, на панель, ваш awesome стал тормозить с реакцией на нажатие клавиш, кнопок мыши и всего прочего. Становится очевидно, что уже по менее очевидным причинам обработка событий находится в одном потоке с обновлением виджетов.

Поиск решения описанной проблемы результатов не дал, лень победила, я удалил всё лишнее из панели, установил conky и не стал заморачиваться, но через пару недель всё равно почему-то вспомнил этот случай, решив разобраться. К сожалению, в С/C++ я абсолютный ноль, исправить сабж напрямую не могу, поэтому дальнейшее содержание поста — простой в реализации костыль.

Сначала отмечу, что вышеизложенная проблема касается только кода, выполняемого прямо в awesome любым возможным способом. Если действие выполняется сторонним скриптом ( и строго через awful.util.spawn ), то почти никаких задержек не будет, чем мы и воспользуемся. Наша задача сводится к тому, чтобы вынести 'тяжелый' код за пределы awesome.

В нашем распоряжении dbus и любой удобный вам инструмент ( в моём случае — scala ).
В добавок к уже хвалёным вещам, разработчики позаботились о слегка упрощающем процесс управления менеджером скрипте — awesome-client. Я использовал его, но можно и, наверное, будет правильней пользоваться dbus напрямую, подключив соответствующую библиотеку. В любом случае, не забудьте в rc.lua сделать глобальными переменные / функции, которые вы в дальнейшем будете использовать.

Первым примером будет виджет, показывающий количество доступных обновлений ПО. Lua-версия порой заметно тормозила весь менеджер и выглядела примерно вот так:

Lua

pacwidget = wibox.widget.textbox()
pacwidget.list = ""
pacwidget.timer = timer({ timeout = 600 })
pacwidget.timer:connect_signal("timeout", 
function()
  local io = { popen = io.popen }
  local s = io.popen("pacman -Qu")
  local str = ''
  local count = 0
  for line in s:lines() do
    count = count + 1
    str = str .. line .. "n"
  end
  pacwidget:set_text(tostring(count))
  pacwidget.list = str
  s:close()
end)
pacwidget.timer:start()
pacwidget.notify = nil
pacwidget:connect_signal("mouse::enter",
function()
  pacwidget.notify = naughty.notify({
    text = pacwidget.list,
    title = "Available updates",
    timeout = 0
  })
end)
pacwidget:connect_signal("mouse::leave",
function ()
  if pacwidget.notify then
    naughty.destroy(pacwidget.notify)
    pacwidget.notify = nil
  end
end)

Нас интересует только функция, выполняющаяся каждый раз по истечению опеределенного времени ( см. 'connect_signal timeout' ).
Для удобства создадим простую иерархию классов. Пока ограничимся только нашими потребностями.

Scala

import sys.process._
trait LuaObject {
	val name: String
	def eval(code: String) =
		("echo "+code) #| "awesome-client" !!
}

Трейт LuaObject ( да, я знаю, что в lua нет объектов ), который реализуют все классы-'обёртки' таблиц lua

abstract class Widget(val name: String) extends LuaObject {
}

Абстрактный класс, который ничего не делает и будет родителем для всех виджетов

abstract class TextWidget(name: String) extends Widget(name) {
	private var _text = ""
	def text = _text
	def text_=(arg: String) {
		_text = arg
		eval(f"$name:set_text('$arg')")
	}
}

Абстрактный класс, который якобы наследует наш pacwidget из rc.lua

import sys.process._
object Pacman extends App {
	val pacman = new Pacman(args.head)
}
class Pacman(name: String) extends TextWidget(name) {
	val result = "pacman -Qu" !!;
	text = result.split('n').length.toString
	eval(f"$name.list = '${result.replace("n", "\n")}'") 
}

Наш обработчик, который при запуске получает в качестве аргумента название виджета в rc.lua и заполняет его нужной информацией.

И, в принципе, всё. Код предельно прост и в подробном объяснении, наверное, не нуждается. Запакуем результат в .jar и поместим в любую угодную вам папку ( в моём случае — 'jars/' в директории с rc.lua ). Создадим функцию, запускающую скрипт, и поставим её на таймер вместо аналогичной lua-функции.

Lua

 creator =
 function(jar, arg, time)
   local path = awful.util.getdir("config").."/jars/"
   local timer = timer({ timeout = time })
   local updater =
   function()
     awful.util.spawn_with_shell("java -jar "..path..jar.." "..arg)
   end
   timer:connect_signal("timeout", updater)
   updater()
   return timer
 end
pacwidget.timer = creator("pacman.jar", "pacwidget", 60)

Принимает на вход название файла-обработчика, название виджета и период; возвращает таймер.

Теперь сделаем вариант чуть сложнеё — будем слушать новые письма на gmail. Добавим скромную обёртку для nauhgty, метод apply в LuaObject и, собственно, сам обработчик события.

Scala
class Naughty extends LuaObject {
	val name = "naughty"
	def notify(arg: String) =
		eval(f"$name.notify({ text = '$arg' })")
}

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

def apply(arg: String) = {
	val result = eval(f"return $name.$arg")
	result.trim match {
		case r if r.startsWith("string") => _.splitAt(_.indexOf(' '))._2.drop(2).dropRight(1)
		case _ => ""
	}
}

Такая реализация не совсем правильная, но нам подойдёт.
В scala object.apply(arg) — это то же самое, что и object(arg)

object Gmail extends App {
	val gmail = new Gmail(args.head, args.tail.head.split(":"))
}
class Gmail(name: String, account: Array[String]) extends TextWidget(name) {
	Authenticator.setDefault(new Authenticator() {
		override def getPasswordAuthentication = new PasswordAuthentication(account(0), account(1).toCharArray)
		}
	)
	{
		var stream:BufferedSource  = null
		try {
			stream = io.Source.fromURL("https://mail.google.com/mail/feed/atom/")
			val result = stream.getLines().mkString
			val count = XML.loadString(result).child.find(_.label == "fullcount") match {
				case Some(x) => x.text
				case None => "-1"
			}
			val value = count.toInt
			val message = if(value == 0)
				"No unread mail"
			else
				if(value == -1)
					"Connection error"
				else {
					if(this("count") < count)
						Globals.naughty.notify("New e-mail")
					value+" unread mails"
			}
			eval(f"$name.count = '$count'")
			eval(f"$name.list = '$message'")
		}
		finally {
			if(stream != null)
				stream.close()
		}
	}
}

Вкратце опишу, что тут происходит: конструктор принимает название поля в rc.lua и массив из двух элементов — логин и пароль пользователя, затем происходит известная из java аутентификация, подключение по url и парсинг текущего кол-ва непрочитанных писем; Остальное, я думаю, всем понятно.

Пакуем в .jar, перемещаем в нашу папку, добавляем виджет в rc.lua

Lua

gmailwidget = wibox.widget.imagebox()
gmailwidget:set_image(beautiful.mail)
gmailwidget.list = ""
gmailwidget.count = "0"
gmailwidget.timer = creator("gmail.jar", "gmailwidget user:password", 60)
gmailwidget.timer:start()
gmailwidget.notify = nil
gmailwidget:connect_signal("mouse::enter",
function()
   gmailwidget.notify = naughty.notify({
     text = gmailwidget.list,
     title = "Mail",
     timeout = 0
   })
end)
gmailwidget:connect_signal("mouse::leave",
function ()
  if gmailwidget.notify then
    naughty.destroy(gmailwidget.notify)
    gmailwidget.notify = nil
  end
end)

Перезапускаем менеджер, вуаля.
Управление awesome wm вне конфигурационного файла

Однако и этот костыль не без изъяна, по крайней мере, если вы будете использовать jvm языки, — если порожденный вами процесс не успевает завершиться до начала следующего, обновляющего тот же самый виджет, то может произойти следующая ситуация:

Скрытый текст

Управление awesome wm вне конфигурационного файла

Но на практике такое вероятно только если вы будете обновлять что-либо каждые n < *время работы предыдущего процесса* секунд. Тут уже потребуются какие-либо оптимизации, которые лично мне делать уже лень.

Дальше можно расширить нашу иерархию, возможности, установить более тесное взаимодействие с менеджером, обработку ошибок и так далее. Заодно сделаете один пункт из TODO.

Спасибо за внимание.

Автор: sperson

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js