Skip to content

Using Lua scripts (Part 11): For loops and cpu chart

Brenden Matthews edited this page May 1, 2024 · 7 revisions

xi: For loops and CPU history chart

Part 1

For loops

For loops are extremely useful when you need to do the same thing a number of times.

There are many ways you can use the FOR keyword, but the way that I use the most is like so:

for i = 1, 10 do
    print (i)
end

The i = 1, 10 is telling the script to perform the code within the loop (everything from the do to the end) for a certain number of times.

When just written as i = 1, 10, the code will be repeated 10 times for every whole number between 1 and 10, starting at 1 and ending at 10 (you can tell the code to count in different increments or even in reverse but I usually find counting by whole numbers to work).

Change the numbers in the line to get different numbers of passes of the loop, different starting points and different ending points.

BUT not only is the loop repeated for the designated number of times, the useful part is that for each pass of the loop the value of "i" changes when it is used inside the loop.

So in our loop above:

  • The loop starts, "i" is set to a value of 1 inside the loop.

  • The code within the loop is executed and we print 1 to the terminal. because i = 1 and we are printing i. (i is a just like other strings we have set values to).

  • The loop repeats, "i" is set to a value of 2 inside the loop, the code is executed, we print 2 to the terminal.

  • Loop repeats, i = 3, print 3 to terminal.

  • Loop repeats, i = 4, print 4 to terminal.

And so on until i = 10 and then the loop is done.

So in the terminal we see

1
2
3
4
5
6
7
8
9
10

What makes this useful is that we can then use the changing values for i in calculations.

Say you wanted a horizontal row of 10 filled in white circles with each circle center 20 pixels away from the next with the first circle starting at coordinates 10, 10.

We would set our colors and set up the cairo_arc function (x, y, radius, start_angle, end_angle) then draw the circle like this:

cairo_set_source_rgba (cr, 1, 1, 1, 1)
cairo_arc (cr, 10, 10, 10, 0, 2 * math.pi)
cairo_fill (cr)

Then we could copy and paste the code a further 9 times and edit the x coordinates for every subsequent circle.

Or we could use a for loop and have it do the repeats for us we can set our color outside of the loop as we want all our circles to be white.

cairo_set_source_rgba (cr, 1, 1, 1, 1)
for i = 1, 10 do
x = 10 + ((i - 1) * 20)
cairo_arc (cr, x, 10, 10, 0, 2 * math.pi)
cairo_fill (cr)
end

In the above code I've separated the calculation for x out of the arc function and set a string called "x" to the value of the equation x = 10 + ((i - 1) * 20).

This equation takes the value of i and subtracts 1 from it (so our first circle is drawn at x = 10) it then multiplies i - 1 by 20, which is the gap we want between our circles, then adds 10, which is the staring x for our first circle.

So the for loop is activated and "i" is set to 1 so our equation works out so that x = 10 and our first circle is drawn at coordinates 10, 10, as we wanted.

The loop repeats and "i" is set to 2. The equation works out so that x = 30 and our next circle is drawn at coordinates 30, 10 (ie 20 pixels further on from our first circle).

The loop continues until "i" = 10 and we end up with 10 circles positioned as we wanted.

Using equations you can do a lot with a simple progression of numbers as you get in this version of the for loop!

Another good use of the for loop is to put data into tables or read the data that is in a table.

Here is another Lua display project to work on that uses these techniques.

The cpu history chart

Conky has the ability to display a chart showing your cpu usage history with just a short command. It takes a little more to do the same thing in lua, but when you are done you have much more flexibility in how to use it.

The first thing we need to be able to do it record out cpu usage over a period of time so that we have something to display. To do this we are going to use a table and we are going to get the data into the table using a for loop.

The type of table we will use is an indexed table, and in this case our table will only contain numbers.

Indexed tables

These kinds of tables usually look like this when you write them out (I'll only put numbers into the table for now, but it can hold other things too): somedata = {13, 44, 25, 26, 10}.

So unlike the "dictionary" takes we used before, this table only has one part to each bit of information. Commas are always used in tables to separate values.

In an indexed table, we are concerned about the order in which the data is in the table. in the table above, position 1 in the table holds the number 13, position 2 holds the number 44 and so on.

To get the data out of the table we have to use a slightly different method: tablename[tableposition].

You have to use the square brackets for this purpose.

Since I called the table "somedata", to get the first entry in the table we put: somedata[1].

NOTE: we can also use the square brackets to get data out of dictionary tables.

dosettings = {
	red = 1,
	green = 0,
	blue = 0,
	alpha = 1, -- Alpha of 0 = full transparent, 1 = full opaque.
	font = "mono",
	fontsize = 12,
}

print (dosettings["font"]) --> Prints "mono" in the terminal.

Getting data into a table

Tables can be constructed by putting data into them via code rather than having to write the consent into the table directly.

We can use the square brackets to get information into tables (just thinking about indexed tables for now).

We can set up a blank table like this: datatable = {}.

Then we can use this code to put the number 16 into the first position of the table datatable[1] = 16 and we can continue adding values using the same method AS LONG AS we don't skip over any positions.

datatable[2] = 18
datatable[3] = 20
datatable[4] = 22

If we were able to look at the contents of "datatable" directly it would look like this: datatable = {16, 18, 20, 22}.

In fact, it is quite easy to take a look at what is in a table using a for loop.

Reading a table with a for loop

The first thing we need to know is how many repeats we need the loop to do in order to see all the values in the table. In the above case we know that we have put 4 values into the table, but sometimes you dont know ahead of time how many things there are in a table.

An easy way to find out is to put # in front of the table name like so.

-- Set blank table.
datatable = {}
-- Put data into table.
datatable[1] = 16
datatable[2] = 18
datatable[3] = 20
datatable[4] = 22
-- Count entries.
entries = #datatable

print (entries) --> 4 is printed to the terminal.

then we can do this

for i = 1, entries do
    print (datatable[i] )
end

So what happening in the loop?

  • Loop starts, "i" = 1 inside the loop, the code in the loop is executed: print (datatable[i]).

  • And since i = 1, we print the number in the first position of the table "datatable" and see 16 printed in the terminal.

  • The loop repeats, "i" = 2: 18 is printed in the terminal.

  • The loop repeats, "i" = 3: 20 is printed to the terminal.

  • The loop repeats, "i" = 4 (the upper limit of the loop since entries = 4): 22 is printed to the terminal.

  • End of loop.

NOTE: the loop is activated and repeats until done before any subsequent code in the script is executed.

So let's think about what is required to get our cpu history chart

The first thing to consider is that we want to information in our cpu table to be persistent from one execution of the lua script to the next and that leads us to think a bit more about how the whole lua script within Conky works!

EXECUTING THE LUA SCRIPT every Conky cycle the entire lua script is executed BUT we can write code in the lua script with conditions, using if statements for example, to control the operation of different bits of code. We already saw this in the setup lines required by the Conky main function.

We used the line

if updates > 5 then
	-- All the code stuff.
end -- if updates > 5.

The point of this condition is so that you don't get a segmentation fault if you try and access cpu values via the lua script.

Conky updates are quite useful for controlling other things too.

Now if we had a setup like this:

function conky_main ()
-- Main Conky function setup lines.
    if updates > 5 then
        -- Set blank table.
        cputable = {}
        -- Put data into table.
        cputable[1] = conky_parse ("${cpu}")
    end -- if updates > 5
-- Main function close out lines.
end 

Then every cycle of Conky, as long as the update number is above 5 the script is executed.

Beginning "updates = 6" (since updates > 5), the table called "cputable" is set blank and then subsequently the cpu usage value is put into it.

The next Conky cycle, "updates = 7", (since updates is still > than 5 and will be for every additional cycle) the Lua script is executed from the beginning and "cputable" is set blank -- again and again the current cpu% is put into it.

Clearly this isnt going to get us anywhere because we are blanking our table each cycle we want the script to "remember" the values for cpu from previous Conky cycles.

SO we have to come up with a situation where the table "cputable" is set up blank only once and not blanked each time the lua script is executed.

Something like this will do it

function conky_main ()
-- Main Conky function setup lines.

	-- Setup cpu table.
	if updates  ==  4 then
		cputable = {}
	end

	if updates > 5 then
		cputable[1] = conky_parse ("${cpu}")
	end
end

There will be only one time when "updates = 4", so only one occasion for the table to be created. Every subsequent Conky cycle "updates" will not equal 4, so the table isn't blanked and will be persistent.

But the next problem is that we are writing (and then overwriting) the current cpu% to position1 in the table each cycle!

Part 2


So we have a persistent table in which we can store our cpu values so that we will have them available to the script so we can display them.

Now we have to get the information into the table in a useful way. Say we wanted to record 10 readings of cpu data, the value now plus the previous 9 values ...

It may have been:

5 -- 9 seconds ago
6
8
6
12
10
8
7
4
3 -- now

The table we want would look like this: cputable = {5, 6, 8, 6, 12, 10, 8, 7, 4, 3}. We want to limit the number of values we are storing.

You could just keep recording values into the table and have the table grow bigger and bigger, but leave Conky on for a few hours and you will have thousands of entries in the table. Big tables like that are inefficient and would almost certainly increase processor drain as the script has to read through those entries every Conky cycle.

We want to specify the number of entires to store and have the script update the table, overwriting the older values with the new ones.

So, a second later (and our current cpu% is 13) our table would still contain 10 entries, but they would have shifted in order: cputable = {6, 8, 6, 12, 10, 8, 7, 4, 3, 13}.

And so on and so on until our 3 has moved all the way to the left and is eventually lost being replaced with more recent values.

So we need to specify how many entries we want and set a string: table_length = 10.

As ever, there are many ways you could go about getting this result ...

When I first wrote a script to give moving bars I wasnt aware of the use of for loops. Wlourf then came up with the following code:

for i = 1, tonumber (table_length) do
	if cpu_table[i + 1] == nil then
		cpu_table[i + 1] = 0
	end
        cpu_table[i] = cpu_table[i + 1]
	if i == table_length then
		cpu_table[table_length] = tonumber (conky_parse ('${cpu}'))
	end
end

This cutting the number of code lines by many hundreds :D

NOTE: Code indenting can help you keep track of whats going on in your code (when you have compound if statements and or loops). Since it is important that whenever you open an if or a loop you end it with end.

But whether I do it or not is hit or miss.

So lets look at what the code is doing bit by bit.

for i = 1, tonumber (table_length) do: here we are opening the for loop,

  • setting the number of repeats for the loop
  • setting where the loop starts and ends

I'm using tonumber (table_length) to make sure that there is no error given.

More often than not I use a "wait and see" approach with tonumber :), ie I tend not to put them in unless it turns out that I need them (or unless I can anticipate a potential problem -- for example, when getting a value using conky_parse its usually a good idea to use tonumber).

this loop will run 10 times, and "i" will take the value of every whole number between 1 and 10 within the loop in order. if cpu_table[i + 1] == nil then cpu_table[i + 1] = 0 end.

This is one of those nil catcher lines because in the next line we will be trying to read the value that our table cpu_table has at position [i + 1].

We need this because if you try and read a table position that doesn't exist you are going to get a nil value, and then when you try and do something with a string that is nil you get an error and the script won't work until it is fixed.

cpu_table[i] = cpu_table[i + 1]: this is how we are going to get our number cycling.

This line will give us nil values because initially the table is empty so if we try and read from table position cpu_table[i + 1], when we start our loop, i = 1.

So essentially we are trying to read position 2 in the table which is empty and therefore nil

The line above has anticipated this so when we read position 2 in cpu_table it is no longer nil; it has a bvalue of 0 instead.

so our loop starts, i = 1. We read position 2 of cpu_table and check to see if it is nil (if cpu_table[i + 1] == nil). If it is nil (which it will be at the start) then we set it to a value of 0 (then cpu_table[i + 1] = 0 end) we set position 1 of cpu_table (cpu_table[ i ]) to the same value found at position 2 (= cpu_table[i + 1]) position 1 in the table = 0.

Our loop continues, i = 2. We read position 3 of cpu_table and check to see if it is nil (if cpu_table[i + 1] == nil). It will be nil to start, so we set it to a value of 0 (then cpu_table[i + 1] = 0 end). We set position 2 of cpu_table (cpu_table[i]) to the same value found at position 3 (= cpu_table[i + 1]); position 2 in the table = 0.

The loop continues in the same fashion, setting up our table on the first run of the script, so that position 1 to 9 are 0. BUT when we get to the last repeat of the loop we activate the next part of the script:

if i == table_length then
	cpu_table[table_length] = tonumber (conky_parse ('${cpu}'))
end

When i = table_length (which we set to 10) then position 10 in the table cpu_table is set to the current cpu% value as read through the conky_parse command.

SO the very first cycle of the lua script, Conky reads a cpu% value of 8 for example ... so once our for loop is all done what would cpu_table look like?

cpu_table = {0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0}

We will in fact have 11 entries in our table because when i = 10 we still executed the first lines within the for loop, creating an entry [i + 1] with a value of 0.

Next we end our for loop with end;

The next Conky cycle the script is run again and our for loop runs this line cpu_table[i] = cpu_table[i + 1], which shifts all the values in our table 1 position to the left.

Then these lines ...

if i == table_length then
	cpu_table[table_length] = tonumber (conky_parse ('${cpu}'))
end

... capture the new reading into the table at position 10.

If the next cpu value is 5 our table will now look like this cpu_table = {0, 0, 0, 0, 0, 0, 0, 0, 8, 5, 0} next cycle cpu% = 6 cpu_table = {0, 0, 0, 0, 0, 0, 0, 8, 5, 6, 0} and so on: shift existing values one place to the left, put latest value into position 10 We now have our record of CPU, now we have to work to display the values!

Part 3

Here is what we have so far, with a couple of additions and I've added a comment here and there.

-- This script draws history graphs.

require("cairo")
require("cairo_xlib")

function conky_main()
	if conky_window == nil then
		return
	end
	local cs = cairo_xlib_surface_create(
		conky_window.display,
		conky_window.drawable,
		conky_window.visual,
		conky_window.width,
		conky_window.height
	)
	cr = cairo_create(cs)
	local updates = tonumber(conky_parse("${updates}"))

	-- Set up tables, need one table for each graph to be drawn.
	if updates == 4 then
		cpu_table = {}
	end
	-- End of table setup.
	if updates > 5 then
		-- You will need to copy and paste this section for each different
		-- 		graph you want to draw.

		-- set table length, ie how many values you want recorded.
		table_length = 10
		for i = 1, tonumber(table_length) do
			if cpu_table[i + 1] == nil then
				cpu_table[i + 1] = 0
			end
			cpu_table[i] = cpu_table[i + 1]
			if i == table_length then
				cpu_table[table_length] = tonumber(conky_parse("${cpu}"))
			end
		end

		-- Call graph drawing function.
		draw_graph(cpu_table, table_length)
		-- End of graph setup.
	end
	cairo_destroy(cr)
	cairo_surface_destroy(cs)
	cr = nil
end

function draw_graph(data, table_length)
	for i = 1, table_length do
		print(data[i])
	end
end

I have started to think about writing a separate function to do the drawing part of the script ... so far I have the function call in the main function (draw_graph (cpu_table, table_length)) and I have the function itself started below the main function.

function draw_graph (data, table_length)
    for i = 1, table_length do
		print (data[i])
    end
end

Right now all I am doing is sending the contents of cpu_table and table_length from the main function to the drawing function.

In the drawing function, the contents of cpu_table are received and put into a table called "data".

I expect to be sending this function more than one table, for example once I have my cpu graph set up, I can use the same method for a memory usage graph, or a graph for each one of my cpu cores ... or whatever else I want to see displayed.

So I'm using a more generic term for my table, and "data" seemed appropriate :).

At this stage I just have a for loop in the drawing function that will print out in the terminal the contents of "data", using the value of table_length as the upper limit of our loop repeats and "i" value.

So I'm going to run the script and make sure that my data is being collected correctly and being sent to my drawing function correctly. I'll have cpu values showing in Conky via ${cpu} so I can watch Conky and the terminal and makes sure the numbers are correct.

And everything seems to be working ... so we can move on and do something with that data so we have to think about what we want to see at the end ... and what we want to make configurable.

Settings. I want to be able to position my graph, and in this case everything will be relative to the bottom left corner.

I want each cpu value to be represented by a vertical bar. I want the color, width and maximum height of the bar to be configurable. 0% will be at the bottom of the bar, 100% at the top.

I want my graph to move from right to left, so that current cpu value is on the right, getting older to the left.

That should do for now ...

The first thing i would do is to write in a settings section into the main function, and add those settings to the function call as well as editing the drawing function to receive the settings. For now I'll just be sending strings.

I'm also going to make a string to hold the value to be displayed instead of using conky_parse directly in the code and I'm going to make a setting to specify max_value in case we want to use something other than % outputs in the main function.

-- You will need to copy and paste this section for each different
-- 		graph you want to draw.

-- SETTINGS 

-- Set coordinates of bottom left corner of graph.
blx, bly = 100, 200

-- Set value to display.
gvalue = conky_parse ('${cpu}')

-- Set max value for the above value.
max_value = 100

-- Set color, red green blue alpha , values 0 to 1.
red, green, blue, alpha = 1, 1, 1, 1 -- Fully opaque white.

-- Set bar width.
width = 2

-- Set height of chart.
cheight = 150

-- Set table length, ie how many values you want recorded.
table_length = 10

-- END OF SETTINGS
for i = 1, tonumber (table_length) do
	if cpu_table[i + 1] == nil then cpu_table[i + 1] = 0 end
	cpu_table[i] = cpu_table[i + 1]
	if i == table_length then
		cpu_table[table_length] = tonumber (gvalue)
	end
end

-- Call graph drawing function. 
draw_graph (cpu_table, max_value, table_length, blx, bly, red, green,
		blue, alpha, width, cheight) 

-- End of graph setup.

And in the drawing function:

function draw_graph (data, max_value, table_length, blx, bly, red,
		green, blue, alpha, width, cheight) 
    for i = 1, #data do
		print (data[i])
    end
end

Remember, when sending strings you have to make sure the numbers of strings you send in the main function is the same number you set (AND IN THE RIGHT ORDER) in the function that is receiving the strings.

NOTE: in the above case I have asked for bar width ... so the total width of the graph will be determined by the number of bars (which is 10 in our example multiplied by width) I could have decided to set total graph width in which case I would have to calculate the width of the individual bars ... as always there are plenty of different ways to go!

THE BARS. There are a few ways we could draw our bars. We could use rectangles, in which case our setting for width would be the width of the rectangle, or we could draw line, in which case width would affect line width.

Also, depending on which way you go, you will have to think about positioning. With the rectangle you set a corner and draw the rectangle relative to that corner. With the line you would set coordinates of the midpoint, in this case the midpoint at the bottom because of the way the setting of line width is applied.

I'm going to go with drawing lines for this graph.

We can go ahead and set up out colors and line width commands we have our loop set up, and we can start to think about how we will get the lines drawn ...

function draw_graph (data, max_value, table_length, blx, bly, red,
		green, blue, alpha, width, cheight) 
cairo_set_source_rgba (cr, red, green, blue, alpha)
cairo_set_line_width (cr, width)
    for i = 1, table_length do
		print (data[i])
    end
end

When writing the code within the for loop, its a good idea to just think about one instance of code. For example, our loop starts and the first value of "i" will be 1 ... so what do we want to happen when "i" = 1 ...?

Well ... if we were to write print (data[1]), we would see the first entry in the "data" which is (in the code so far) the first value in cpu_table. It is the oldest cpu value that our table contains.

We want our oldest value on the left, and we want the graph relative to the bottom left corner coordinates, so this all works out nicely :).

BUT we do have the hiccup of line width to think about.

Say we did this to draw a vertical line 100 pixels long:

cairo_set_line_width (cr, 10)
cairo_move_to (cr, 100, 200)
cairo_line_to (cr, 100, 100)
cairo_stroke (cr)

our "move to" coordinates would be the bottom MIDDLE of the line. The line would extend for 1 / 2 line width to the left and 1 / 2 line width to the right.

So the actual coordinates of the bottom left corner of the line would be 95, 200.

To put that as a calculation:

-- Set value of blx.
blx = 100

-- Calculate actual coordinate to use with line so that bottom left x
-- 		is actually  at 100.
blx = blx + (width / 2)

NOTE we are adding (width / 2) to blx because we want to shift the line to the right a little (increasing x means moving right).

The second instance of "blx =" uses the original blx in the calculation but then overwrites the contents of the blx string with the new value.

bly will stay the same for each line we draw so we can construct an initial cairo_move_to line replacing, out print (data[ i ]) test line like so: cairo_move_to (cr, blx + (width / 2), bly).

This would be the start position for the leftmost bar ...

Now we have to think about how we can apply the changing values of "i" inside the for loop to set the starting positions for all the bars.

From the bottom-left corner of the first line to the bottom-left corner of the next line will be equal to our line width and this will be true for each subsequent bar.

So we will be multiplying "width" by the value of "i".

BUT cairo_move_to (cr, blx + (width / 2), bly) is what we want for the first bar.

And since we are using i = 1, table_length, the first number "i" will be is 1. So to keep out first start point as it is we would do something like this: cairo_move_to (cr, blx + (width / 2)+((i - -) * width), bly).

"Why not just use for i = 0, 9? you ask. "Then we could just cairo_move_to (cr, blx + (width / 2) + (i * width), bly)".

No particular reason ... but then you would have to go and make our other for loops "for i = 0, 9" because right now if we were to try and read "data[i]" and i = 0, we would get a nil value.

I almost always start my for loops at 1 and calculate relative to that :). Of course, there are times when you want to start at 0 or a different number.

So in our code so far we can put:

function draw_graph (data, max_value, table_length, blx, bly, red,
		green, blue, alpha, width, cheight) 
cairo_set_source_rgba (cr, red, green, blue, alpha)
cairo_set_line_width (cr, width)
    for i = 1, table_length do
		cairo_move_to (cr, blx + (width / 2) + ((i - 1) * width), bly)
    end
end

The next thing to do, just in the cpu indicator bar example, is calculate bar height based on the value of cpu and the settings for total graph height (which I called "cheight") and max value.

In this case, however, the values are in the table "data", so we need to get then out inside the for loop, like this: bar_height = (cheight / max_value) * data[i].

  • We have a start position for each bar
  • We know how tall to draw each bar

We just need to actually draw the lines, and in this case I'm going to use cairo_rel_line_to.

cairo_rel_line_to draws a line relative to the coordinates we set in cairo_move_to. In this case we want the x value of each line to be the same (as we are drawing vertical lines) and we want the y value to change and represent our cpu value.

  • for the rel_line, to draw UP we have to specify NEGATIVE values

So put all that together our drawing function looks like this:

function draw_graph (data, max_value, table_length, blx, bly, red,
		green, blue, alpha, width, cheight) 
	cairo_set_source_rgba (cr, red, green, blue, alpha)
	cairo_set_line_width (cr, width)
	for i = 1, table_length do

		-- Calculate bar height.
		bar_height = (cheight / max_value) * data[i]

		-- Set start position for each bar, and modify with the value
		-- 		of "i".
		cairo_move_to (cr, blx + (width / 2) + ((i - 1) * width), bly)

		-- Draw relative line, y becomes equal to bar height and
		-- 		must be negative to draw up. 
		cairo_rel_line_to (cr, 0, bar_height*-1)

		-- Draw the line.
		cairo_stroke (cr)
	end
end 

And watch your cpu chart in action!

Clone this wiki locally