It is currently October 21st, 2021, 4:59 am

Todo.txt, and approaches to file i/o...

Discuss the use of Lua in Script measures.
FlyingHyrax
Posts: 232
Joined: July 1st, 2011, 1:32 am
Location: US

Todo.txt, and approaches to file i/o...

Post by FlyingHyrax »

[update]
I've got this working to my satisfaction.
[/update]


This is a structure question, rather than about s specific piece of code. Just looking for some guidance from all you Lua gurus...

I'm working on a Lua script to interface with Todo.txt (yes, I'm aware that it's been done).

Right now, I have the skin read the todo.txt file, process the strings into a table of task objects, and set Text field of string meters in the skin using that information. It's been educational to implement, but the overall idea is pretty straightforward.

My question is regarding file IO in the other direction, so to speak. If I want to alter a task or mark it as complete from the skin, how should the script handle that?

My thoughts so far go roughly like this -
1) I can have script methods write straight to the todo.txt/done.txt files, then just re-read the file contents (perhaps even just refresh the skin) to update the tasks displayed in the skin. I have to do this already on initialization to build the task table, so I would just have to re-call those methods after writing changes...
2) I could have the changes occur in the script first - alter the tasks inside the table of tasks, so that I can update the tasks displayed in the skin without having to do any file IO. Then periodically "flush" changes to the actual todo.txt file - for instance, have a "destructor" function called by OnCloseAction to write the altered task list to todo.txt when the skin is closed, and/or on refresh.

#1 is quick and dirty, but it is so, so simple and it does have the advantage that your todo.txt file would always be up to date.
#2 seems neater and might enable better response times, since the file doesn't have to be re-read when you make a change. But there's a big lag effect and it's more complicated.

The middle ground might be to change table -> update skin from table -> write to file (but don't re-read the file). But at that point, since you already have the file open to write changes, how much more overhead is incurred by re-reading it and just refreshing the table from that? (#1)

Thanks in advance for any feedback.

Oh, and if you want to have a look at the Spaghetti so far:

Code: Select all

dbg = true

function Initialize()	
	path = SELF:GetOption('TodoPath','.\todo.txt')
	dateSwitch = tonumber(SELF:GetOption('UseDate', '0'))	-- unimplemented
	
	lineMeters = {}
	priMeters = {}
	taskMeters= {}
	
	for i=1, 10, 1 do 
		table.insert(lineMeters, SKIN:GetMeter('line' .. i))
		table.insert(priMeters, SKIN:GetMeter('pri' .. i))
		table.insert(taskMeters, SKIN:GetMeter('task' .. i))
	end
	
	rawLines = readLinesFromFile(path)
	rawLines = removeBlankLines(rawLines)
	
	TodoList = TaskList:createListFromLines(rawLines)
	
--	testing filtering	
--	temp = TodoList:searchFilter("")
	
	TodoList:sortByPri()
	populateSkin(TodoList)
	
end

function Update()
	--...
end

-- read lines from todo.txt into a simple table
function readLinesFromFile(fp)
	local temp = {}
	for line in io.lines(fp) do
		table.insert(temp, line)
	end
	return temp
end

-- removes blank lines from table of lines
function removeBlankLines(lineTable)
	for i,v in ipairs(lineTable) do
		if v == "" then
			table.remove(lineTable, i)
		end
	end
	return lineTable
end

-- sets text and shows meters
function populateSkin(atable)
	for i,v in ipairs(atable) do
		if i > #taskMeters then
			break
		end
		-- TODO make line numbers optional
		SKIN:Bang('!SetOption', lineMeters[i]:GetName(), 'Text', v.lineNo)
		lineMeters[i]:Show()
		if v.priority ~= nil then
			SKIN:Bang('!SetOption', priMeters[i]:GetName(), 'Text', v.priority)
			priMeters[i]:Show()
		end
		SKIN:Bang('!SetOption', taskMeters[i]:GetName(), 'Text', v.taskString)
		taskMeters[i]:Show()
		
	end
end


-- does not support timestamping.  We're working on it.
function addTask(taskStr)
	io.output(io.open(path, "a"))
	io.write('\n' .. taskStr)
	io.flush()
	io.close()
	SKIN:Bang('!Refresh')
end

-- not implemented at all. Will probably be painful.
function doTask(taskNum)

end


--[=[-------------------------------------------------------------------------------
Attempt at a "TaskList" class.
TaskList objects hold Task objects and have methods to build/process lists.
Then the skin/script interface bits can be used to display a given TaskList.

! Currently list is sorted in place; should a copy be returned instead?
! TODO filter by project and/or context
---------------------------------------------------------------------------------]=]

-- TaskList namespace/prototype (empty)
TaskList = {}

-- TaskList constructor
function TaskList:new (lst)
	lst = lst or {}
	setmetatable(lst, self)
	self.__index = self
	return lst
end

-- should create a new TaskList of Tasks from a Table of single-line strings
function TaskList:createListFromLines(strings)
	newList = TaskList:new ()
	
	for i,v in ipairs(strings) do
		table.insert(newList, Task:newTask(v, i)) 	-- uses posiiton in table as line number
	end
	
	return newList
end

-- sorts self by Priority then alpahbetically
function TaskList:sortByPri()
	table.sort(self, function (te1, te2)
		if (te1.priority ~= nil and te2.priority ~= nil) then
			if (te1.priority < te2.priority) then
				return true
			else
				return false
			end
			
		elseif (te1.priority ~= nil and te2.priority == nil) then
			return true
			
		elseif (te1.priority == nil and te2.priority ~= nil) then
			return false
			
		else
			if (te1.taskString < te2.taskString) then
				return true
			else
				return false
			end
		end
	end)
end

-- sorts self by Age
function TaskList:sortByAge()
	table.sort(self, function (te1, te2)
		if te1.startDate == nil and te2.startDate == nil then
			if te1.taskString < te2.taskString then
				return true
			else
				return false
			end
		elseif te1.startDate < te2.startDate then 
			return true
		else
			return false
		end
	end)
end

-- sorts self by line number
function TaskList:sortByLine()
	table.sort(self, function (te1, te2)
		if te1.lineNo < te2.lineNo then
			return true
		else 
			return false
		end
	end)
end

-- returns a table containing tasks filtered by a search term
function TaskList:searchFilter(term)
	local temp = {}
	for i,v in ipairs(self) do
		if string.find(v.taskString, term) then
			table.insert(temp, v)
		end
	end
	return temp
end

-- add a new task to the TaskList
-- does NOT add the task to the file
-- not sure how to handle line numbers.
function TaskList:addTask(taskStr)
	table.insert(self, Task:newTask(taskStr, #self+1))
end

-- removes a task from the tasklist by line number
-- does NOT mark the task as complete or remove it from the todo.txt file
function TaskList:removeTask(lineNum)
	for index, task in ipairs(self) do
		if task.lineNo == lineNum then
			table.remove(self, index)
			break
		end
	end
end


-- end TaskList class
--[=[ ------------------------------------------------------------------------------
Attempt at a "Task" class to better organize this mess.
Passed compliler, but so far no idea if it works.
Has an object constructor to make new Task objects use the defined Task as a prototype and metatable
Has a method to take a string and line number and transform it into a new task object

Would like to add more methods as necessary, 
one idea would return the Lifetime of a completed task (completeDate - startDate) ?
---------------------------------------------------------------------------------]=]

-- define namespace for "Task" class (with defaults)
Task = {taskString="", lineNo=nil, priority=nil, completed=false, completeDate=nil, startDate=nil, context={}, project={}}

-- constructor function 
function Task:new (tsk)
	tsk = tsk or {}		-- creates a new table/object if not given one
	setmetatable(tsk, self) 	-- uses itself as metatable?
	self.__index = self		
	return tsk
end

-- creates a new task from a single line string
function Task:newTask (str, lineNum)
	local t = Task:new()	-- create a new task object
	t.taskString = str
	t.lineNo = lineNum
	
	-- eat first four characters
	tempStr = string.sub(t.taskString, 1, 4)
	-- search first four chars for complete or priority
	if string.sub(tempStr, 1, 2) == 'x ' then
		t.completed = true
		t.taskString = string.sub(t.taskString, 3)
	else
		t.priority = string.match(tempStr, "%(%u%) ")
		if t.priority then
			t.taskString = string.sub(t.taskString, 5)
		end
	end
	
	-- search task string for dates
	datePat = "%d%d%d%d%-%d%d%-%d%d"
	if t.completed then
		tempStr = string.sub(t.taskString, 1, 22)	-- completed task may have two dates
		t.completeDate, t.startDate = string.match(tempStr, "(" .. datePat .. ") (" .. datePat .. ") ")
	else 
		tempStr = string.sub(t.taskString, 1, 11)	-- incomplete may only have one (start date)
		t.startDate = string.match(tempStr, datePat .. " ")
	end
	-- strip dates from task text
	if startDate then
		t.taskString = string.sub(t.taskString, 12)
	end
	if completeDate then
		t.taskString = string.sub(t.taskString, 12)
	end
	
	-- search for context(s)	!bugged! word boundaries?
	for match in string.gmatch(t.taskString, "@(%w+)") do
		table.insert(t.context, match)
	end
	
	-- search for project(s) !bugged! word boundaries?
	for match in string.gmatch(t.taskString, "%+(%w+)") do
		table.insert(t.project, match)
	end
	
	return t	-- return the newly created and filled Task object
end

-- marks this task as complete
-- does NOT remove the task from the file or TaskList
function Task:markComplete()
	self.completed = true
	self.completeDate = os.date("%Y-%m-%d")
end

-- changes priority of this task to the letter passed as argument,
-- or deprioritizes the task if passed nil/false
-- does NOT update todo.txt file
function Task:changePriority(newPri)
	if not newPri then 
		self.priority = nil
	elseif not string.find(newPri, "%a") then
		error("Bad argument for new task priority - must be a letter.", 2)
	else		
		self.priority = "(" .. string.upper(newPri) .. ")"
	end
end

-- end Task class
Flying Hyrax on DeviantArt