UProLa

Неокріпші думки

Паскаль на букву … Py!

with 5 comments

Сьогодні поговоримо про Пайтон і про те, як на Пайтоні написати простеньку curses гуйовину.

Python + curses у ролі Pascal + CRT – погляд сучасності

Досі пам’ятаю, скільки щастя надавав модуль crt у дитинстві. Можливість відмальовки різноколірного тексту, прекрасний clrscr;, потужний gotoxy()… Це були справді одні з кращих моментів програмування на той момент. Тоді ж було вперше для себе відкрито такі поняття як “псевдографіка”, “елементи керування”, “select” і наскільки широко їх можна використовувати для візуалізації моделі програми.

Зараз же, враховуючи повальний інтерес до піксельного графічного інтерфейсу, консольний графічний інтерфейс відійшов на третій план (графічний, консоль, псевдографіка). Виною тому, мабуть, недостатня гнучкість при порівняно високих затратах на кодинг (в порівнянні з консоллю). В принципі, це правильно, проте існуюють випадки (як у мене, наприклад), коли потрібна виразність, а не гнучкість, і вибір стоїть між графікою і псевдографікою. Дещо нижче розгляну це детальніше.

Python замість Pascal – вчити старе по новому

Давненько уже в інтернеті деякі подумують, як ввести Пайтон основною мовою програмування для навчання в школах. Цій ідилії заважає величезна купа факторів реального світу, але ідея, в загальному, хороша.

Візьмемо, для прикладу, модуль turtle. Можна сміливо заявити – це сучасна повноцінна заміна мові LOGO. Я на ньому навіть графіки малював, оскільки спеціалізовані модулі все ще не переведено на Python3.

import turtle

def plotAxes(screen):
    turtle.reset()
    turtle.up()
    turtle.goto(0,0)
    turtle.down()
    turtle.goto(screen.screensize()[0], 0)
    turtle.up()
    turtle.goto(0, 0)
    turtle.down()
    turtle.goto(0, 1)


def plot(pointsY):
    screen = turtle.Screen()
    maxy = max(pointsY)
    miny = min(pointsY)
    screen.setworldcoordinates(-10, miny*1.1, len(pointsY)+10, maxy*1.1)

    plotAxes(screen)

    turtle.hideturtle()
    turtle.speed(0)
    turtle.up()
    turtle.goto(0, pointsY[0])
    turtle.down()
    i = 0
    for point in pointsY:
        turtle.goto(i, point)
        i += 1
    turtle.mainloop()

Python кращий ніж Pascal – довзоляє легко працювати з системою

Мабуть, завдяки скриптовій природі і принципу “simple is better than complicated”, Пайтон впевнено просувається у галузі “швидких програмок” – маленьких і простеньких софтинок. При зовсім невеликих знаннях і досвіді, мені вдалося за вечір написати просту і потрібну (мені, звісно) утиліту маніпуляції зовнішніми моніторами.

Завдання було простим: ввімкнути зовнішній монітор (або проектор), підключений до ноутбуку. Виявилось, що для цього достатньо виконати одну команду:

xrandr --output VGA-0 --mode 1680x1050

А для вимикання:

xrandr --output VGA-0 --off

(Нюанс. Графічних програм управління моніторами у мене немає – вони не підходять під ідеологію мінімалістичного інтерфейсу. Тому все потрібно налаштовувати з консолі)

Проте тут постала важлива проблема. По-перше, оперція підключення монітору є, по-суті, рідкісною. Або один раз поставили два монітори на робочому місці, або раз в кілька місяців підключаєте зовнішній монітор для отримання яскравіших вражень від відео. По-друге, рано чи пізно ви можете змінити ноут, і вам доведеться на ньому наново налаштовувати систему. Тобто, рішення проблеми повинно бути здатним до мігрування між системами і бути легким для використання, без перелистування тонн мануалів one more time.

multiscreen.py

Рішенням стало написання скрипта на Пайтоні і позначення його як системного. Якщо колись знадобиться ввімкнути зовнішній монітор, то завдяки псевдографічному консольному інтерфейсу мені не доведеться згадувати конкретні консольні команди. А написання програми “в загальному” повинно допомогти при міграції.

Проте, як ви уже здогадались, основною причиною було спробувати використати модуль curses для більш-менш реальної задачі. =)

Каркас для curses

Як би це дивно не звучало, але мій шлях почався з цього посилання – http://cybportal.univ.kiev.ua/wiki/Python_curses. Дещо модифікований і адаптований варіант приводжу нижче:

#!/usr/bin/python3
import curses
import curses.wrapper

def main(screen):
  curses.nonl()

  while True:
    ### відрисовка
    s = curses.newwin(5, 20, 20, 1)
    s.keypad(1) # режим спрощеного вводу клавіш-стрілочок
    s.box()
    s.refresh()

    ### обробка
    key = s.getch()
    if key in (ord("q"), 27):
      break
    s.erase()

if __name__ == '__main__':
  try:
    curses.wrapper(main)
  except KeyboardInterrupt:
    # не виводити стек, якщо натиснули Ctrl-C
    pass

Взаємодія з Linux – аналіз виводу xrandr

Приведу для прикладу свій вивід утиліти xrandr:

[danbst@localost ~]$ xrandr
Screen 0: minimum 320 x 200, current 1366 x 768, maximum 8192 x 8192
LVDS connected 1366x768+0+0 (normal left inverted right x axis y axis) 270mm x 150mm
   1366x768       60.0*+
   1280x720       59.9  
   1152x768       59.8  
   1024x768       59.9  
   800x600        59.9  
   848x480        59.7  
   720x480        59.7  
   640x480        59.4  
HDMI-0 disconnected (normal left inverted right x axis y axis)
VGA-0 connected (normal left inverted right x axis y axis)
   1680x1050      60.0 +
   1600x1200      60.0  
   1280x1024      75.0     60.0  
   1440x900       75.0     59.9  
   1280x960       60.0  
   1152x864       75.0  
   1280x720       60.0  
   1024x768       75.1     70.1     60.0  
   832x624        74.6  
   800x600        72.2     75.0     60.3     56.2  
   640x480        72.8     75.0     66.7     60.0  
   720x400        70.1  
[danbst@localost ~]$ 

Перший рядок показує, що ми маємо в системі тільки один віртуальний екран (до-речі, з віртуальними моніторами також було би корисно розібратись). Наступні рядки перелічують відеороз’єми і режими до них.

Для отримання списку відео-роз’ємів я навіть написав sed-скрипт, але він на жаль не знадобився…

[danbst@localost ~]$ xrandr | sed -r -e '1d; /^\s/d; s/(^[^ ]+).*/\1/'
LVDS
HDMI-0
VGA-0
[danbst@localost ~]$

Тепер про режими. Плюсик означає, що режим є дефолтним для монітору. Зірочка означає, що даний режим зараз активний. Отриманої інформації достатньо, щоб написати функції вмикання і вимикання монітору. До-речі, був би вдячний, якби хтось підказав спосіб спростити дані процедури😉

#!/usr/bin/python3
import subprocess

def get_monitors():
  """Перетворити інформацію, яку видає xrandr, у хеш: ключ ім’я монітору, значення - список режимів"""
  xrandr_data = subprocess.check_output("xrandr",universal_newlines=True)
  monitors = []
  for line in str(xrandr_data).split("\n"):
    if "connected" in line:
      monitors.append([line.split()[0], []])
    elif line.startswith(' '):
      monitors[-1][1] += [line]
  mons = {}
  for monitor in monitors:
    mons[monitor[0]]  = monitor[1]
  return mons

def get_default_monitor(monitor_name):
  """Отримати дефолтний режим монітору, у вигляді NNNNxNNNN"""
  monitors = get_monitors()
  return [x for x in monitors[monitor_name] if "+" in x][0].strip().split()[0]

def is_monitor_on(monitor_name):
  """Визначити, чи горить монітор"""
  monitors = get_monitors()
  return len([x for x in monitors[monitor_name] if "*" in x]) > 0

def toggle_monitor(monitor_name):
  """Ввімкнути/вимкнути монітор. Вибирається дефолтне розширення екрану"""
  monitors = get_monitors()
  # все робимо у try ... catch, бо ліньки було розбирати всі варіанти, коли неможливо визначити дефолтний монітор
  try:
    if not is_monitor_on(monitor_name):
      subprocess.call(["xrandr", "--output", monitor_name, "--mode", get_default_monitor(monitor_name)])
    else:
      subprocess.call(["xrandr", "--output", monitor_name, "--off"])
  except:
    pass

Псевдографічне меню

Я довго не міг зрозуміти, як правильніше за все відмальовувти менюшку. В кінці-кінців вирішив малювати її в окремому вікні і при кожній ітерації перемальовувати. Без поняття, як інакше зробити візуальний режим вибору елементу.

def main(screen):
  curses.nonl()
  selected = 0
  items = list(get_monitors().keys())

  while True:
    ### відрисовка
    s = curses.newwin(len(items) + 2, 20, 20, 1)
    s.keypad(1) # режим спрощеного вводу клавіш-стрілочок
    s.box()
    for i in range(0, len(items)):
      if i == selected:
        s.addstr(i+1, 1, items[i], curses.A_STANDOUT)
      else:
        s.addstr(i+1, 1, items[i])
    s.refresh()

    ### обробка
    key = s.getch()
    if key in (ord("q"), 27):
      break
    elif key == curses.KEY_DOWN:
      selected = min(selected + 1, len(items) - 1)
    elif key == curses.KEY_UP:
      selected = max(selected - 1, 0)
    elif key == 13:
      toggle_monitor(items[selected])
    s.erase()

Виявилось, це зовсім не важко робити, якщо не ускладнювати життя (по початковій задумці, я хотів динамічні checkbox-и, щоб стан монітору виводився біля пункту елементу). Виглядає це приблизно так (браузер приведено для порівняння розмірів):

Звісно, працює зручний перехід між пунктами за допомогою клавіш-курсорів =)

Весь код

Наостанок приведу весь код разом, щоб бажаючі могли протестувати у себе. А раптом відпишуть баг-репорт =) Ще одна особливість – код працює однаково і на 2 і на 3 пітоні

#!/usr/bin/python2
# -*- coding: UTF-8 -*-
import curses
import curses.wrapper
import subprocess

def get_monitors():
  """Перетворити інформацію, яку видає xrandr, у хеш: ключ ім’я монітору, значення - список режимів"""
  xrandr_data = subprocess.check_output("xrandr",universal_newlines=True)
  monitors = []
  for line in str(xrandr_data).split("\n"):
    if "connected" in line:
      monitors.append([line.split()[0], []])
    elif line.startswith(' '):
      monitors[-1][1] += [line]
  mons = {}
  for monitor in monitors:
    mons[monitor[0]]  = monitor[1]
  return mons

def get_default_monitor(monitor_name):
  """Отримати дефолтний режим монітору, у вигляді NNNNxNNNN"""
  monitors = get_monitors()
  return [x for x in monitors[monitor_name] if "+" in x][0].strip().split()[0]

def is_monitor_on(monitor_name):
  """Визначити, чи горить монітор"""
  monitors = get_monitors()
  return len([x for x in monitors[monitor_name] if "*" in x]) > 0

def toggle_monitor(monitor_name):
  """Ввімкнути/вимкнути монітор. Вибирається дефолтне розширення екрану"""
  monitors = get_monitors()
  # все робимо у try ... catch, бо ліньки було розбирати всі варіанти, коли неможливо визначити дефолтний монітор
  try:
    if not is_monitor_on(monitor_name):
      subprocess.call(["xrandr", "--output", monitor_name, "--mode", get_default_monitor(monitor_name)])
    else:
      subprocess.call(["xrandr", "--output", monitor_name, "--off"])
  except:
    pass


def main(screen):
  curses.nonl()
  selected = 0
  items = list(get_monitors().keys())

  while True:
    ### відрисовка
    s = curses.newwin(len(items) + 2, 20, 20, 1)
    s.keypad(1) # режим спрощеного вводу клавіш-стрілочок
    s.box()
    for i in range(0, len(items)):
      if i == selected:
        s.addstr(i+1, 1, items[i], curses.A_STANDOUT)
      else:
        s.addstr(i+1, 1, items[i])
    s.refresh()

    ### обробка
    key = s.getch()
    if key in (ord("q"), 27):
      break
    elif key == curses.KEY_DOWN:
      selected = min(selected + 1, len(items) - 1)
    elif key == curses.KEY_UP:
      selected = max(selected - 1, 0)
    elif key == 13:
      toggle_monitor(items[selected])
    s.erase()

if __name__ == '__main__':
  try:
    curses.wrapper(main)
  except KeyboardInterrupt:
    # не виводити стек, якщо натиснули Ctrl-C
    pass

Written by danbst

Листопад 5, 2011 at 01:58

Оприлюднено в Програмування

Відповідей: 5

Subscribe to comments with RSS.

  1. Поскольку любимая мной кадэешечка по какой-то загадочной причине в закладке Multimonitor пишет всегда “this system does not have multiple monitors connected” при любом раскладе, я давно пользуюсь xrandr’ом и все команды запомнились. Хотя, только что я понял, почему не работает KDE апплет. Он берёт описания из xorg.conf, а откуда ж им там появиться при динамическом подключении монитора?

    Есть один момент, который здесь не учтён. По умолчанию xrandr включает второй монитор справа от основного и они размещаются в памяти Screen 0. Однако, если мониторы сильно не совпадают по режимам, будет удобнее разместить их друг над другом, к примеру. Тогда к параметрам после режима добавляются ключи –above VGA-0 или –below VGA-0 или –left-of, –right-of. Ещё бывает полезен режим миррора: –same-as VGA-0. Помню, когда защищал диплом, при подключении проектора, –auto неверно что-то определил, пришлось быстро набирать –same-as. При этом важно, чтобы мониторы “поместились” в Screen 0: minimum 320 x 200, current 1280 x 800, maximum 4096 x 4096 — на разных компах по разному. К примеру, на стареньком компике у меня на работе два монитора не влазят по ширине🙂

    Александр Щапов

    Листопад 5, 2011 at 09:52

  2. про нюансик відомо, але я поки-що не знайшов простого способу візуалізації left-of, right-of, above, below. Хоча, якби була статична система з двух однакових моніторів – щось та й придумав би😉

    Серйозніша проблема – розбиття на скріни. Хотілось би, щоб була можливість пересилати один з робочих столів на інший монітор.

    danbst

    Листопад 5, 2011 at 10:02

  3. Про пересылать это ты уже сам ищи в ратпойзоне. Потому как КДЕшечка всё клёво умеет на разных столах.

    Про визуализацию:

    +------------ 4096 x 4096 -----------------------------------------+
    | +------ 1280 x 720 ----- +---- 1024 x 768 ----+                  |
    | |           LVDS -1      |        VGA 0       |                  |
    | |                        |                    |                  |
    | |                        |                    |                  |
    | |                        |                    |                  |
    | |                        |                    |                  |
    | +------------------------+                    |                  |
    |                          +------------------- +                  |
    +------------------------------------------------------------------+
    

    Александр Щапов

    Листопад 5, 2011 at 10:08

    • я мав на увазі не візуалізацію результату, а візуалізацію процесу власне розбиття ) краще лишити це на совість консольного налаштування.

      ratpoison вміє посилати робочі столи на інші скріни, прооблема в тому, що він розпізнає тільки один… У виводі xrandr в пості першим рядком йде “Screen 0: …”. Думаю, скріни задаються в xorg.conf, хоча можу помилятись.

      danbst

      Листопад 5, 2011 at 10:33


Залишити відповідь

Заповніть поля нижче або авторизуйтесь клікнувши по іконці

Лого WordPress.com

Ви коментуєте, використовуючи свій обліковий запис WordPress.com. Log Out / Змінити )

Twitter picture

Ви коментуєте, використовуючи свій обліковий запис Twitter. Log Out / Змінити )

Facebook photo

Ви коментуєте, використовуючи свій обліковий запис Facebook. Log Out / Змінити )

Google+ photo

Ви коментуєте, використовуючи свій обліковий запис Google+. Log Out / Змінити )

З’єднання з %s

%d блогерам подобається це: