Skip to content

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

lasers edited this page Jan 4, 2019 · 7 revisions

xii: 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 operator, but the way that I use the most is like so: for example

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

so i called the table "somedata", so 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={--set table and open
red=1,
green=0,
blue=0,
alpha=1,--alpha of 0 = full transparent, 1=full opaque
font="mono",
fontsize=12,
}--closes table

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 is 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 we 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 lets 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-- end main function

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-- if updates>5
--main function close out lines
end-- end main function

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 isnt blanked and will be persistent from

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--of for loop

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

NOTE - code indenting. 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

im 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 wont 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 positioncpu_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 end--of for loop

the next conky cycle the script is run again and our for loop runs this line cpu_table[i]=cpu_table[i+1]

shifts all the values in our table 1 position to the left and 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 ive added a comment here and there

--this script draws history graphs

require 'cairo'

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}'))
--setup 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--of for loop
--call graph drawing function
draw_graph(cpu_table,table_length)
--end of graph setup
--#########################################################################################################
end-- if updates>5
cairo_destroy(cr)
cairo_surface_destroy(cs)
cr=nil
end-- end main function

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

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--of function draw_graph

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 im 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 im 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

im also going to make a string to hold the value to be displayed instead of using conky_parse directly in the code and im 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--of for loop
--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--of function draw_graph

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

im 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--of function draw_graph

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 and1/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-1)*width),bly)

why not just use "for i=0,9" ? you ask so that we can just put 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--of function draw_graph

the next thing to do, just in the cpu indicator bar example, we need to calculate bar height based on the value of cpu and the setting 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 using the square brackets like this inside the for loop 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 im 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--of function draw_graph

and watch your cpu chart in action!

Clone this wiki locally