Gjs, пишем продвинутый "hello world"

10.05.2020 02:13

В этот раз попробуем написать что-нить сложнее hello world. И разобраться с некоторыми моментами.

Вёрстка

Вёрстка в gtk чем-то похожа на веб. Размеры окна и элементов автоматически подстраиваются и автоматически выравниваются, в зависимости от того, какой контейнер используется:

Gtk.Box - горизонтальный контейнер, элементы добавленные в Gtk.Box, выстраиваются по горизонтале.

Gtk.Grid - что-то вроде html таблицы, с колонками и строками.

Gtk.ListBox - вертикальный контейнер

Gtk.Stack и Gtk.StackSwitcher - я тут ещё не разобрался нахер оно надо, но очень интересно.

Gtk.FlowBox - что-то вроде флексбокса, когда колонки автоматически выстраиваются, в зависимости от свободного пространства.

Нам понадобится простой Gtk.Box, который мы разобьём на 2 части, в левую поместим фрейм, во фрейм графику, в правую то же фрейм но со списком.

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GLib  = imports.gi.GLib;

GLib.set_prgname('Advanced Hello World on gjs');

let app = new Gtk.Application({ application_id: 'org.gtk.advancedHello' });

app.connect('activate', () => {
  log('App started');
  
  let mainWin = new Gtk.ApplicationWindow({ application: app });
  let container = new Gtk.Box({});
  
  let leftFrame = new Gtk.Frame({});
  let rightFrame = new Gtk.Frame({});
  
  container.add(leftFrame);
  container.add(rightFrame);
  
  leftFrame.set_size_request(500, 300);
  rightFrame.set_size_request(250, 300);
  
  mainWin.add(container);
  mainWin.show_all();
});

app.run([]);

Результат:

Теперь идём на https://github.com/optimisme/gjs-examples смотрим примеры, находим egList.js и берём его за основу, для построения тестового списка.


function cellFuncText1(col, cell, model, iter) {
  cell.editable = false;
  cell.text = model.get_value(iter, 1);
};

function cellFuncText2(col, cell, model, iter) {
  cell.editable = false;
  cell.text = model.get_value(iter, 2);
};

function getBody(parent) {
  parent.scroll = new Gtk.ScrolledWindow({ vexpand: true });

  parent.store = new Gtk.ListStore();
  parent.store.set_column_types([GObj.TYPE_INT, GObj.TYPE_STRING, GObj.TYPE_STRING, GObj.TYPE_BOOLEAN]);
  parent.store.set(parent.store.append(), [0, 1, 2, 3], [0, '0A', 'Name 0', false]);
  parent.store.set(parent.store.append(), [0, 1, 2, 3], [1, '1B', 'Name 1', false]);
  parent.store.set(parent.store.append(), [0, 1, 2, 3], [2, '2C', 'Name 2', false]);
  parent.store.set(parent.store.append(), [0, 1, 2, 3], [3, '3D', 'Name 3', false]);

  parent.tree = new Gtk.TreeView({ headers_visible: false, vexpand: true, hexpand: true });
  parent.tree.set_model(parent.store);  
  parent.scroll.add(parent.tree);

  parent.col = new Gtk.TreeViewColumn();
  parent.tree.append_column(parent.col);

  let text1 = new Gtk.CellRendererText();
  parent.col.pack_start(text1, true);
  parent.col.set_cell_data_func(text1, (col, cell, model, iter) => { cellFuncText1(col, cell, model, iter); });

  let text2 = new Gtk.CellRendererText();
  parent.col.pack_start(text2, true);
  parent.col.set_cell_data_func(text2, (col, cell, model, iter) => { cellFuncText2(col, cell, model, iter); });

  return parent.scroll;
};
...
rightFrame.add(getBody(this));

Результат:

Дорабатываем ListStore/TreeView, читаем файл

Т.к. gjs- это не nodejs, то для работы с файлами применяется своя библиотека - GLib, и похоже, функции сюда перекочевали из php, например, для синхронного чтения файла существует функция file_get_contents.

Давайте напишим парсилку cpuinfo, а затем в колонки раскидаем инфу о частоте процессора:

function parseCPUInfo(text) {  
  let result = [];
  let lines = text.toString().split('
');  
  
  let curProcessor = 0;
  for(let line of lines) {
    
    let parts = line.split(':');
    if( parts.length <= 1 )
      continue;
    
    let key = parts[ 0 ].trim();
    let val = parts[ 1 ].trim();
    
    if( key === 'processor' )
      curProcessor = parseInt(val);
    
    if( !result[ curProcessor ] )
      result[ curProcessor ] = {};
    
    result[ curProcessor ][ key ] = val;
  }
  
  return result;
}
...

function getBody(parent, cpuInfo) {
  parent.scroll = new Gtk.ScrolledWindow({ vexpand: true });
  
  parent.store = new Gtk.ListStore();
  parent.store.set_column_types([GObj.TYPE_INT, GObj.TYPE_STRING, GObj.TYPE_STRING, GObj.TYPE_BOOLEAN]);
  
  for(let index=0; index<cpuInfo.length; index++)
    parent.store.set(parent.store.append(), [0, 1, 2, 3], [index, `C${cpuInfo[index].processor}`, cpuInfo[index]['cpu MHz'], false]);

  parent.tree = new Gtk.TreeView({ headers_visible: true, vexpand: true, hexpand: true });
  parent.tree.set_model(parent.store);  
  parent.scroll.add(parent.tree);

  parent.col = new Gtk.TreeViewColumn({ title: 'CPU' });
  parent.col2 = new Gtk.TreeViewColumn({ title: 'MHz' });
  parent.tree.append_column(parent.col);
  parent.tree.append_column(parent.col2);

  parent.text1 = new Gtk.CellRendererText();
  parent.col.pack_start(parent.text1, true);
  parent.col.set_cell_data_func(parent.text1, (col, cell, model, iter) => { cellFuncText1(col, cell, model, iter, cpuInfo); });

  parent.text2 = new Gtk.CellRendererText();
  parent.col2.pack_start(parent.text2, true);
  parent.col2.set_cell_data_func(parent.text2, (col, cell, model, iter) => { cellFuncText2(col, cell, model, iter, cpuInfo); });

  return parent.scroll;
};

Результат:

Ещё нам нужно как-то обновлять данные в уже созданных ячейках. Для этого есть функция ListStore.foreach(), которая последовательно пройдёт по всем строкам и вызовет функцию с параметрами (model, box, iter)

function updateCells(parent, cpuInfo) {
  parent.store.foreach(function(model, box, iter) {
    let index = model.get_value(iter, 0);
    parent.store.set(iter, [1, 2], [`C${cpuInfo[index].processor}`, cpuInfo[index]['cpu MHz']]);
  });
}

Gjs модули

У нас стало много кода. Давайте разобьём проект на модули, которые сложим в в отдельный коталог "modules".

В отличии от nodejs, в экспорт попадают все переменные и функции, но остаются они в своём неймспейсе, не нужно юзать module.exports.

// ./modules/cpuinfo.js
const GLib  = imports.gi.GLib;

const procPath = '/proc/cpuinfo';

function parseCPUInfo(text) {
  let result = [];
  let lines = text.toString().split('
');  
  
  let curProcessor = 0;
  for(let line of lines) {
    
    let parts = line.split(':');
    if( parts.length <= 1 )
      continue;
    
    let key = parts[ 0 ].trim();
    let val = parts[ 1 ].trim();
    
    if( key === 'processor' )
      curProcessor = parseInt(val);
    
    if( !result[ curProcessor ] )
      result[ curProcessor ] = {};
    
    result[ curProcessor ][ key ] = val;
  }
  
  return result;
}

function getCPUInfo() {
  let [ok, contents] = GLib.file_get_contents(procPath);
  return parseCPUInfo(contents);
}

А чтобы подключить модули, нужно сперва указать каталог, где кастомные модули распаложены, и уже потом можно подключить модуль:

imports.searchPath.push('./'); 
const getCPUInfo = imports.modules.cpuinfo.getCPUInfo; 

getBody и всё что с ним связано тоже уносим в отдельный модуль. В котором мы будем использовать getCPUInfo. И вот уже внутри сабмодуля, чтобы подключить свой модуль, ещё раз указывать imports.searchPath.push('./'); не нужно, работает родительская декларация:

Теперь наш app.js будет такой:

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GLib  = imports.gi.GLib;
const GObj  = imports.gi.GObject;

imports.searchPath.push('./');
const getCPUInfo = imports.modules.cpuinfo.getCPUInfo;
const getBody = imports.modules.getBody.getBody;

let app = new Gtk.Application({ application_id: 'org.gtk.advancedHello' });

app.connect('activate', () => {
  log('App started');
  
  let mainWin = new Gtk.ApplicationWindow({
    application: app,
    window_position: Gtk.WindowPosition.CENTER,
    title: 'Advanced Hello World on gjs'
  });
  let container = new Gtk.Box({});
  
  let leftFrame = new Gtk.Frame({});
  let rightFrame = new Gtk.Frame({});
  
  container.add(leftFrame);
  container.add(rightFrame);
  
  leftFrame.set_size_request(500, 300);
  rightFrame.set_size_request(250, 300);
 
  rightFrame.add(getBody(this));
  
  mainWin.add(container);
  mainWin.show_all();
});

app.run([]);

Gjs setTimeout и setInterval

А вот нет этих функций. Их придётся реализовать самим, а точнее ещё раз честно спиздить с примеров: https://github.com/optimisme/gjs-examples/blob/master/assets/timers.js.

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

...
const setInterval = imports.modules.timers.setInterval;
...
app.connect('activate', () => {
  ...
  let updateInterval = setInterval( () => {
    let cpuInfo = getCPUInfo();
    updateCells(this, cpuInfo);
  }, 1000);
 ...
});

И происходит чудо!

Рисуем чарт на Gjs

Наступает самые интересный и сложный момент. Сложный потому, что я не имею понятия как работать с графикой, интересный, потому, что графика- это всегда интересно.

После нескольких часов гугления и разбора примеров, я решил, что буду использовать cairo, просто потому, что нашлось хотя бы 2.5 примера использования его именно с gjs, а не чистым gtk.

Сперва переведём из C++ в gjs этот пример:

// C++
public static int main (string[] args) {
	// Create a context:
	Cairo.ImageSurface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, 80, 80);
	Cairo.Context context = new Cairo.Context (surface);

	// Draw a rectangle:
	context.rectangle (10, 10, 50, 50);
	context.set_source_rgba (0, 0, 0, 1);
	context.set_line_width (8);
	context.fill ();

	// Change some style settings inside save/restore:
	context.set_operator (Cairo.Operator.CLEAR);
	context.set_source_rgba (1, 0, 0, 1);
	context.rectangle (20, 20, 50, 50);
	context.fill ();

	// Save the image:
	surface.write_to_png ("img.png");

	return 0;
}

// Gjs
const Gdk = imports.gi.Gdk;
const cairo = imports.cairo;

function main() {
  let surface = new cairo.ImageSurface (cairo.Format.ARGB32, 80, 80);
  let context = new cairo.Context (surface);
  
  context.rectangle (10, 10, 50, 50);
  context.setSourceRGBA (0, 0, 0, 1);
  context.setLineWidth (8);
  context.fill ();
  
  context.setOperator (cairo.Operator.CLEAR);
  context.setSourceRGBA (1, 0, 0, 1);
  context.rectangle (20, 20, 50, 50);
  context.fill ();

  // Save the image:
  surface.writeToPNG ("img.png");
  return false;
}
main();

Программа создасть маленькую пнгшечку "img.png":

Т.к. нам нужно выводить изображение не в файл, а в окно приложения, то cairo контекст нужно создать по другому:

let context = Gdk.cairo_create(drawing_area.get_window());
// drawing_area - это какой-либо GTK UI элемент, в моём случае это 
let drawArea = new Gtk.DrawingArea();
drawArea.connect('draw', chart.drawChart);
//которую я вешаю на 
leftFrame.add(drawArea);

И ещё немного покурив доку находим всё необходимое. Контекст очищается с помощью такой конструкции:

  context.operator = cairo.Operator.CLEAR;
  context.paint();
  context.operator = cairo.Operator.OVER;

Цвет линии устанавливается оператором context.setSourceRGBA(r, g, b,a), а линия рисуется вот так:

context.setLineWidth (1);
context.moveTo(xStart, yStart);
context.lineTo(x, y );
context.stroke();

Теперь остаётся делом техники собрать всё воедино:

// ./modules/chart.js
const Gdk = imports.gi.Gdk;
const cairo = imports.cairo;

const colors = [
  [190, 0, 0, 0.5],
  [0, 190, 0, 0.5],
  [0, 0, 190, 0.5],
  [190, 0, 190, 0.5],
  [190, 190, 0, 0.5],
  [0, 190, 190, 0.5],
  [255, 0, 0, 0.5],
  [0, 255, 0, 0.5],
  [0, 0, 255, 0.5],
  [128, 0, 255, 0.5],
  [0, 128, 255, 0.5],
  [255, 128, 0, 0.5],
  [255, 0, 128, 0.5],
  [0, 255, 128, 0.5],
  [128, 255, 0, 0.5],
  [128, 255, 128, 0.5],
];

function cleanSurface(drawArea) {
  let surface = drawArea.get_window();
  let context = Gdk.cairo_create(surface);
  
  context.operator = cairo.Operator.CLEAR;
  context.paint();
  context.operator = cairo.Operator.OVER;
}

function getMaxMinVal(chartStore, maxWidth) {
  let maxVal = 0;
  let minVal = 9999999;
  
  for(let index=(chartStore.length-1); (index>=0 && (index>=(chartStore.length-maxWidth))); index--) {
    for(let cpuIndex=0; cpuIndex<chartStore[index].length; cpuIndex++) {
      if( chartStore[ index ][ cpuIndex ][ 'cpu MHz' ] > maxVal )
        maxVal = chartStore[ index ][ cpuIndex ][ 'cpu MHz' ];
      
      if( chartStore[ index ][ cpuIndex ][ 'cpu MHz' ] < minVal )
        minVal = chartStore[ index ][ cpuIndex ][ 'cpu MHz' ];
    }
  }
  
  return [ maxVal, minVal ];
}

function calcHeight(val, maxVal, minVal, surfaceHeight) {
  let realLength = val - minVal;
  let realMaxLength = maxVal - minVal;
  
  return surfaceHeight - parseInt( (realLength/realMaxLength) * surfaceHeight );
}

function drawChart(drawArea, parentFrame, chartStore, cpuCount) {
  
  if( chartStore.length <2 )
    return;
  
  let surface = drawArea.get_window();
  let context = Gdk.cairo_create(surface);
  let [ width, height ] = parentFrame.get_size_request();
  
  context.operator = cairo.Operator.CLEAR;
  context.paint();
  context.operator = cairo.Operator.OVER;
  
  let [ maxVal, minVal ] = getMaxMinVal(chartStore, width);  
  context.setLineWidth (1);
  
  let colorIndex = 0;
  for(let cpuIndex=0; cpuIndex<cpuCount; cpuIndex++) {
    context.setSourceRGBA (...colors[ colorIndex ]);
    
    let startVal = chartStore[ chartStore.length-1 ][ cpuIndex ]['cpu MHz'];
    context.moveTo(width - 1, calcHeight(startVal, maxVal, minVal, height));
    
    let xOffset = 1;
    for(let index=chartStore.length-1; ( (index>=0) && (index>(chartStore.length-width)) ); index--) {
      let val = chartStore[ index ][ cpuIndex ]['cpu MHz'];
      let pHeight = calcHeight(val, maxVal, minVal, height);
      context.lineTo(width - xOffset, pHeight );
      xOffset ++;
    }
    context.stroke();
    
    colorIndex ++;
    if( colorIndex >= colors.length )
      colorIndex = 0;
  }
  
  return false;
} 

Результат:

Заключение

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

Да и получился какой-то уж очень продвинутый hello world, и, наверное, я его буду постепенно допиливать до совсем красивого состояния.

Исходники лежать тут: https://gitlab.com/hololoev/gjs_cpufreqinfo

vk f tw in

vk f tw in

gtk + javascript = gjs, делаем "hello world"

Давно хотел посмотреть какой-нибудь гуёвый фреймворк, а тут оказалось, что под gtk можно писать на javascripte. Мне показалось это на много интереснее чем electron.

Новый шаблон Hapi.js REST сервера

Я уже фигачил пачку статей про REST сервер на hapi. Но с тех пор прошло много времени. Обновились модули, обновился мой опыт. Вот на основе нового опыта я и собрал новый шаблон, с ещё более переиспользуемым кодом и с обновлёнными модулями.


(0) Комментариев