It is currently March 28th, 2024, 10:15 pm

Talking with Lua...

Discuss the use of Lua in Script measures.
User avatar
jsmorley
Developer
Posts: 22628
Joined: April 19th, 2009, 11:02 pm
Location: Fort Hunt, Virginia, USA

Talking with Lua...

Post by jsmorley »

The Lua language itself is quite powerful and very easy to learn, even if you have very little experience in any kind of programming.
https://docs.rainmeter.net/manual-beta/lua-scripting/

One thing that is specific to Rainmeter, and won't be found in any Lua tutorials, is the communication between the skin and the Lua script it is running. How does your skin talk to the Lua, and how does the Lua talk to your skin?

There are several ways to accomplish this, and I thought I might briefly touch on them.

First, some "style" guidelines for Lua

1) Lua is always, and I mean ALWAYS, case sensitive. We can actually take advantage of this, using differing case formats to help us keep track of what is what.

a) In the skin, everything should be camel-case. So [MeterName] and #VariableName# as examples.
b) In the Lua, both your function names and functions specific to our interface should be camel-case. So MyFunction() and GetValue() as examples.
c) Built-in library functions in Lua like return or tonumber() and statement elements like if, then, else, end are always just lower case.
d) In the Lua, variable names should be a modified camel-case, with the first character in lower case. So myVariable and timeString as examples.

Just never, ever forget that myVariable, MyVariable, myvariable, and MYVARIABLE are all complete different things. Forgetting this can only lead to tears...

2) Use proper indenting for functions and code "blocks" in Lua. Tabs or spaces, that's your karma... But indent, it makes it all so much easier to follow.

Code: Select all

function MyFunction()

	if myIncrement == 100 then
		myIncrement = 0
	end
	
end
3) Enclose strings in Lua using single quote ' characters, not double quote " characters. This will be of great value when you are communicating back and forth with Rainmeter in !Bangs, as Rainmeter expects double quotes and will ignore single quotes. So myString = 'Some String' as an example.

4) Use variable names in Lua that give some hint of what they are. You are not paying for them by the character, and there is NO benefit to "terse" when naming variables in Lua. This is my personal bugaboo, but why use sec=30, when secondsToWait=30 is so much easier to follow.

Ok enough "style" hints that half of you will disagree with and the other half will ignore... ;-)

↓↓↓ A .rmskin with all the examples ↓↓↓
TalkingWithLua_1.0.rmskin
Download this, and you can load the various skins to follow along and use as reference later.


For the most part, the explanations for what we are doing are included as --Comments in the .lua files.

1 - A simple return
https://docs.rainmeter.net/manual-beta/lua-scripting/#Update

The most straightforward way to get some value, some string or number, that you created or modified in the Lua back to your skin is to simply use the return someValue function built-into Lua. When a Lua script measure is updated in the skin, it will always look for an Update() function in the script file. It will execute everything in the function, and at the end, it will execute a return function that will set the value of the Script measure to any value you want.

So we have an example skin SimpleReturn.ini:

Code: Select all

[Rainmeter]
Update=1000
DynamicWindowSize=1
AccurateText=1

[Variables]

[MeasureLuaScript]
Measure=Script
ScriptFile=#@#LuaScripts\SimpleReturn.lua

[MeterCountTo10]
Meter=String
MeasureName=MeasureLuaScript
FontSize=13
FontWeight=400
FontColor=255,255,255,255
SolidColor=47,47,47,255
Padding=5,5,5,5
AntiAlias=1
Text=The current value is %1
And it is updating the Lua script file SimpleReturn.lua on each skin update:

Code: Select all

function Initialize()

	-- Initialize the variable inc to zero, so we can do math with it in the Update() function.
	inc = 0

end

function Update()

	-- On each update of the script measure in the skin, add 1 to the variable inc.
	inc = inc + 1
	
	-- Since we are counting in a loop from 1 to 10, if the value of inc is 11, set it  to 1.
	if inc == 11 then 
		inc = 1
	end
	
	-- Return the current value of inc as the string and number value of the script measure.
	return inc

end
1.png
2 - Using !Bangs to talk to the skin
https://docs.rainmeter.net/manual-beta/lua-scripting/#Bang

Any bang in Rainmeter can be executed from the Lua. This can be used with !SetOption or !SetVariable to change options or values in the skin based on the work the Lua does.

So we have an example skin BangReturn.ini:

Code: Select all

[Rainmeter]
Update=1000
DynamicWindowSize=1
AccurateText=1

[Variables]

[MeasureLuaScript]
Measure=Script
ScriptFile=#@#LuaScripts\BangReturn.lua

[MeterCountTo10]
Meter=String
FontSize=13
FontWeight=400
FontColor=255,255,255,255
SolidColor=47,47,47,255
Padding=5,5,5,5
AntiAlias=1
Text=Let's Count to 10
And it is updating the Lua script file BangReturn.lua on each skin update:

Code: Select all

function Initialize()

	-- Initialize the variable inc to zero, so we can do math with it in the Update() function.
	inc = 0

end

function Update()

	-- On each update of the script measure in the skin, add 1 to the variable inc.
	inc = inc + 1
	
	-- Since we are counting in a loop from 1 to 10, if the value of inc is 11, set it  to 1.
	if inc == 11 then 
		inc = 1
	end

	-- Use a !SetOption bang to set the text value of the meter MeterCountTo10 to the value
	-- of the variable inc, with some leading text.
	SKIN:Bang('!SetOption', 'MeterCountTo10', 'Text', 'The current value is '..inc) 
	
	-- Just for fun, let's set the color of the meter MeterCountTo10 based on the value.
	if inc == 1 then
		SKIN:Bang('!SetOption', 'MeterCountTo10', 'FontColor', '247,245,178,255')
	elseif inc == 4 then
		SKIN:Bang('!SetOption', 'MeterCountTo10', 'FontColor', '167,250,152,255')
	elseif inc == 7 then
		SKIN:Bang('!SetOption', 'MeterCountTo10', 'FontColor', '152,203,250,255')
	elseif inc == 10 then
		SKIN:Bang('!SetOption', 'MeterCountTo10', 'FontColor', '240,29,29,255')
	end

	-- Then !UpdateMeter the meter and !Redraw the skin.
	SKIN:Bang('!UpdateMeter', 'MeterCountTo10') 
	SKIN:Bang('!Redraw')
	
	-- Note that we are not using the return function at all. By default, the return function,
	-- and thus the value of the Script measure, will be zero if not defined.

end
2.png
So the first two are all about how you talk TO the skin FROM the Lua. Let's look at how we might talk FROM the skin TO the Lua.

3 - Using [Measure] values

In order to use a [Measure] value in the Lua script, you must do two things in the Lua:

1) Create a "handle" object to the measure. This is what will be referenced later to get the current value of the measure. This can and should be done in the Initialize() function of the Lua script, as you only need to do it once, not on every update of the Script measure.
https://docs.rainmeter.net/manual-beta/lua-scripting/#GetMeasure

2) Use this handle in conjunction with a GetValue() (number) or GetStringValue() (string) function in the Update() function of the Lua when you want the current value of the measure.
https://docs.rainmeter.net/manual-beta/lua-scripting/#GetValue
https://docs.rainmeter.net/manual-beta/lua-scripting/#GetStringValue

So we have an example skin MeasureValue.ini:

Code: Select all

[Rainmeter]
Update=1000
DynamicWindowSize=1
AccurateText=1

[Variables]

[MeasureCalc]
Measure=Calc
Formula=(MeasureCalc % 10) + 1

[MeasureLuaScript]
Measure=Script
ScriptFile=#@#LuaScripts\MeasureValue.lua

[MeterAdd100ToMeasure]
Meter=String
MeasureName=MeasureLuaScript
FontSize=13
FontWeight=400
FontColor=255,255,255,255
SolidColor=47,47,47,255
Padding=5,5,5,5
AntiAlias=1
And it is updating the Lua script file MeasureValue.lua on each skin update:

Code: Select all

function Initialize()

	-- Initialize a "handle" to the measure [MeasureCalc] in the skin, so we can retrieve its value later.
	myMeasure = SKIN:GetMeasure('MeasureCalc')

end

function Update()

	-- On each update of the script measure in the skin, get the current value of [MeasureCalc].
	-- Note that MeasureHandle:GetValue() obtains number values,
	-- and MeasureHandle:GetStringValue() obtains string values.
	currentValue = myMeasure:GetValue()
	
	-- Add 100 to that value.
	currentValuePlus100 = currentValue + 100
	
	-- And return both of them to the Script measure as a string.
	return 'The current value is '..currentValue..' plus 100 is '..currentValuePlus100

end
3.png
4 - Using #Variables#
https://docs.rainmeter.net/manual-beta/lua-scripting/#GetVariable

Another way to communicate back and forth from the skin to the Lua is by using [Variables] from the skin. Lua can both read the value of a static or changing #VariableName# from the skin, and can set them using the !SetVariable bang.

So we have an example skin VariableExchange.ini:

Code: Select all

[Rainmeter]
Update=1000
DynamicWindowSize=1
AccurateText=1

[Variables]
StaticVariable=100
ChangingVariable=0

[MeasureCalc]
Measure=Calc
Formula=(MeasureCalc % 10) + 1
OnUpdateAction=[!SetVariable ChangingVariable "[MeasureCalc]"]

[MeasureLuaScript]
Measure=Script
ScriptFile=#@#LuaScripts\VariableExchange.lua

[MeterDisplayVariables]
Meter=String
FontSize=13
FontWeight=400
FontColor=255,255,255,255
SolidColor=47,47,47,255
Padding=5,5,5,5
AntiAlias=1
And it is updating the Lua script file VariableExchange.lua on each skin update:

Code: Select all

function Initialize()

	-- First, let's get the value of the skin variable StaticVariable. It's not going to change,
	-- so we only need to get it once, in Initialize().
	staticVar = SKIN:GetVariable('StaticVariable')

end

function Update()

	-- On each update of the Script measure in the skin, we get the current value
	-- of the skin variable ChangingVariable. This will be changed by the counter in the 
	-- [MeasureCalc] Calc measure on each skin update.
	changingVar = SKIN:GetVariable('ChangingVariable')
	
	-- Let's add them together to get a total.
	totalVar = staticVar + changingVar
	
	-- Now we set the value of a skin variable TotalValue in the skin.
	-- Note that it doesn't have to already exist in [Variables] in the skin,
	-- as !SetVariable will create it if needed.
	SKIN:Bang('!SetVariable', 'TotalValue', totalVar)
	
	-- Then set the Text option of the [MeterDisplayVariables] meter, using the 
	-- variables StaticVariable and ChangingVariable we got from the skin, and
	-- telling it to use the current value of the new #TotalValue# variable we are setting.
	SKIN:Bang('!SetOption', 'MeterDisplayVariables', 'Text', 'Lua sees StaticVariable as '..staticVar..'#CRLF#Lua sees ChangingVariable as '..changingVar..'#CRLF#Lua set TotalVariable to #TotalValue#') 

	-- Then !UpdateMeter the meter and !Redraw the skin.
	SKIN:Bang('!UpdateMeter', 'MeterDisplayVariables') 
	SKIN:Bang('!Redraw')
	
end
4.png
5 - Using SELF options on the Script measure
https://docs.rainmeter.net/manual-beta/lua-scripting/#SelfObject

Another way to pass information from the skin to the Lua is to make use of the SELF: object functionality built-into our Lua to Rainmeter interface. What this allows you to do is set a custom option on the Script measure, with any string or number value you want, and access this value in the Lua script with myString=SELF:GetOption('MyOption') or myNumber=SELF:GetNumberOption('MyOption')
https://docs.rainmeter.net/manual-beta/lua-scripting/#GetOption
https://docs.rainmeter.net/manual-beta/lua-scripting/#GetNumberOption

So we have an example skin UsingSELF.ini:

Code: Select all

[Rainmeter]
Update=1000
DynamicWindowSize=1
AccurateText=1

[Variables]

[MeasureCalc]
Measure=Calc
Formula=(MeasureCalc % 10) + 1

[MeasureLuaScript]
Measure=Script
ScriptFile=#@#LuaScripts\UsingSELF.lua
AmountToAdd=200

[MeterAddSomeAmountToMeasure]
Meter=String
MeasureName=MeasureLuaScript
FontSize=13
FontWeight=400
FontColor=255,255,255,255
SolidColor=47,47,47,255
Padding=5,5,5,5
AntiAlias=1
And it is updating the Lua script file UsingSELF.lua on each skin update:

Code: Select all

function Initialize()

	-- Initialize a "handle" to the measure [MeasureCalc] in the skin, so we can retrieve its value later.
	myMeasure = SKIN:GetMeasure('MeasureCalc')
	
	-- Get the value of the custom option AmountToAdd from the Script measure.
	valueToAdd = SELF:GetNumberOption('AmountToAdd')

end

function Update()

	-- On each update of the script measure in the skin, get the current value of [MeasureCalc].
	-- Note that MeasureHandle:GetValue() obtains number values,
	-- and MeasureHandle:GetStringValue() obtains string values.
	currentValue = myMeasure:GetValue()
	
	-- Add the value of valueToAdd to that value.
	currentValuePlus100 = currentValue + valueToAdd
	
	-- And return both of them to the Script measure as a string.
	return 'The current value is '..currentValue..' plus '..valueToAdd..' is '..currentValuePlus100

end
5.png
6 - Calling a custom Lua function with !CommandMeasure
https://docs.rainmeter.net/manual-beta/lua-scripting/#CommandMeasure

There are times when you don't want the Lua to be driven by the Update cycle of the skin, or the UpdateDivider modifier on the Script measure, but want to execute the Lua "on demand". This might be when some [Measure] value reaches some "target" you define in an IfCondition, or it might be when you click on something with a LeftMouseUpAction. You want the work that the Lua does to be specific to the action being taken, and only executed when needed.

At a high-level, this is done by using !CommandMeasure to execute a FunctionName() in the Lua. You simply use !CommandMeasure, telling it the [MeasureName] of the Script measure hosting the .lua file, and then a string that fully defines the FunctionName() and any parameters to it you need.

So we have an example skin FunctionCall.ini:

Code: Select all

[Rainmeter]
Update=1000
DynamicWindowSize=1
AccurateText=1

[Variables]

[MeasureTime]
Measure=Time
Format=%#I:%M:%S %p

[MeasureLuaScript]
Measure=Script
ScriptFile=#@#LuaScripts\FunctionCall.lua
UpdateDivider=-1

[MeterTimeOne]
Meter=String
FontSize=13
FontWeight=400
FontColor=255,255,255,255
SolidColor=47,47,47,255
Padding=5,5,5,5
AntiAlias=1
Text=I'm One, Click Me
LeftMouseUpAction=[!CommandMeasure MeasureLuaScript "ShowClickTime('[MeasureTime]', '#CURRENTSECTION#')"]

[MeterTimeTwo]
Meter=String
Y=10R
FontSize=13
FontWeight=400
FontColor=255,255,255,255
SolidColor=47,47,47,255
Padding=5,5,5,5
AntiAlias=1
Text=I'm Two, Click Me
LeftMouseUpAction=[!CommandMeasure MeasureLuaScript "ShowClickTime('[MeasureTime]', '#CURRENTSECTION#')"]
And we make a specific call to the ShowClickTime() function in the Lua script when we click on the meters:
Note that we don't have any Initialize() or Update() functions in the script at all, as we are not using them.

Code: Select all

function ShowClickTime(timeString, meterName)

	-- In the function definition above, we are telling it to expect be passed some 
	-- value for a new Lua variable timeString, and a new Lua variable meterName.
	-- These will be passed in the !CommandMeasure action in the skin as strings.
	-- timeString will be the current string value of the measure [MeasureTime],
	-- and meterName will be the #CURRENTSECTION# name of the meter the 
	-- mouse action is on.
	
	-- Note that variables in Lua are dynamically "typed". That means that if you
	-- want Lua to see the variables timeString and meterName defined above 
	-- as string values, you MUST pass them explicitly as strings when
	-- calling the function. The !CommandMeasure bang in the skin does this:
	
	-- [!CommandMeasure MeasureLuaScript "ShowClickTime('[MeasureTime]', '#CURRENTSECTION#')"]
	
	-- See that we are enclosing the parameters to the script function() in single quote ' characters,
	-- to explicitly tell Lua to treat these values as strings when they are received.
	
	-- Then we demonstrate that we got them by setting the Text option on the 
	-- appropriate meterName with the current value of timeString.
	SKIN:Bang('!SetOption', meterName, 'Text', 'Clicked at '..timeString)
	
	-- Now we !UpdateMeter the meter we had passed to the Lua in meterName.
	SKIN:Bang('!UpdateMeter', meterName)
	-- And !Redraw the skin.
	SKIN:Bang('!Redraw')

end
6.png
You do not have the required permissions to view the files attached to this post.
User avatar
eclectic-tech
Rainmeter Sage
Posts: 5384
Joined: April 12th, 2012, 9:40 pm
Location: Cedar Point, Ohio, USA

Re: Talking with Lua...

Post by eclectic-tech »

Very useful and informative (as always)! :D

Thanks for sharing! :thumbup:
RicardoTM
Posts: 216
Joined: December 28th, 2022, 9:30 pm
Location: México

Re: Talking with Lua...

Post by RicardoTM »

Hey teacher I got a question.

What if the arg is a dynamic variable used on a SetOption bang and you want it to stay dynamic?

Something like [!SetOption meter fontcolor "[*&Script:function('[*#Color*]')*]"]

What's the correct syntax for that?
User avatar
Yincognito
Rainmeter Sage
Posts: 7029
Joined: February 27th, 2015, 2:38 pm
Location: Terra Yincognita

Re: Talking with Lua...

Post by Yincognito »

RicardoTM wrote: March 18th, 2024, 3:06 pm Hey teacher I got a question.

What if the arg is a dynamic variable used on a SetOption bang and you want it to stay dynamic?

Something like [!SetOption meter fontcolor "[*&Script:function('[*#Color*]')*]"]

What's the correct syntax for that?
I think the correct way to write it would be [!SetOption meter fontcolor "[&*Script:function('[#*Color*]')*]"] (notice the order of &, #, and *). Also, in case you need this one day, you can "multiescape" things, like, for example, [#**Color**] and such (useful if you nest escaped stuff in variables)...
Profiles: Rainmeter ProfileDeviantArt ProfileSuites: MYiniMeterSkins: Earth
RicardoTM
Posts: 216
Joined: December 28th, 2022, 9:30 pm
Location: México

Re: Talking with Lua...

Post by RicardoTM »

Yincognito wrote: March 18th, 2024, 4:58 pm I think the correct way to write it would be [!SetOption meter fontcolor "[&*Script:function('[#*Color*]')*]"] (notice the order of &, #, and *). Also, in case you need this one day, you can "multiescape" things, like, for example, [#**Color**] and such (useful if you nest escaped stuff in variables)...
Hey Yin, Interesting, In my first tests it seemed to me that [*#Color*] was exactly the same as [#*Color*]. Since it didn't appear to make any difference. Same for [*&Measure*] and [&*Measure*]. But [*&Measure[*#Color*]*] is not the same as [&*Measure[#*Color*]*] apparently.... Anyway, any of these syntaxes solved my problem.

The thing is that the function is trying to solve literal [#*Color*] on first load and it logs attempt to perform arithmetic on a nil value, But it works fine. I just want to get rid of the error.

I've tried [!SetOption Meter Option " "]:

"[&*Measure:function('[#Variable]')*]" - Variable no longer dynamic, no errors.
"[*&Measure:function('[#Variable]')*]" - Same outcome.

"[&*Measure:function('[*#Variable*]')*]" - Does not work at all, no errors.
"[*&Measure:function('[*#Variable*]')*]" - Same outcome.

"[&*Measure:function('[#*Variable*]')*]" - Works fine, dynamic variable, logs error on first load.
"[*&Measure:function('[#*Variable*]')*]" - Does not work at all, no errors.

So, "[&*Measure:Function('[#*Variable*]')*]" is the only one that works but logs an error because it's trying to solve [#*Variable*] literally at first.

If I use it in a measure and that measure on a meter, for example:

Code: Select all

[DynamicColor]
Measure=String
String=[&*Measure:function('[#*DynamicVariable*]')*]
DynamicVariables=1

[Meter]
Meter=String
FontColor=[&DynamicColor]
DynamicVariables=1
It works fine, no errors at all.

All of this leads me to think that although it may not be a bug, it may very well be a flaw on the Escaping syntax exclusively on set options or, much more probable, there's a lot more I need to learn.

Edit:

It seems like the fix is: [&**Measure:function('[#**DynamicVariable**]')**]

So it would be:

[!SetOption meter SomeMouseAction """[!SetOption meter Option [&**Measure:function('[#**DynamicVariable**]')**]][!Update]"""]

As you mentioned
Yincognito wrote: March 18th, 2024, 4:58 pm Also, in case you need this one day, you can "multiescape" things, like, for example, [#**Color**] and such (useful if you nest escaped stuff in variables)...
This was the day, that's when the "multiescape" is necessary I guess. Yeah.. I forgot to mention I was using it in a "double set option bang" on a conditional action.

Makes sense. When the condition is true, it sets the option and immediately tries to solve the function, but it can't read the correct variable as it's escaped. Then when you actually mouse over it, the variable is no longer escaped and it succeeds. So the "multiescape" avoids it from "running " the function when the condition is true but it isn't necessary yet.

Thanks for your help, as always.
User avatar
Yincognito
Rainmeter Sage
Posts: 7029
Joined: February 27th, 2015, 2:38 pm
Location: Terra Yincognita

Re: Talking with Lua...

Post by Yincognito »

RicardoTM wrote: March 19th, 2024, 8:30 am Thanks for your help, as always.
Frankly, I had no idea that multi escaping would solve your problem here, I only mentioned it so that you know for the future, but I'm glad that the time was right and it did. You're welcome, as always. ;-)
Profiles: Rainmeter ProfileDeviantArt ProfileSuites: MYiniMeterSkins: Earth
RicardoTM
Posts: 216
Joined: December 28th, 2022, 9:30 pm
Location: México

Re: Talking with Lua...

Post by RicardoTM »

Yincognito wrote: March 19th, 2024, 7:31 pm Frankly, I had no idea that multi escaping would solve your problem here, I only mentioned it so that you know for the future, but I'm glad that the time was right and it did. You're welcome, as always. ;-)
How would you if I didn't even explain the problem correctly :lol:. You hit the nail anyway.