Warning: there may be occasional oddness due to css and blog edits. **KNOWN ISSUE: possible hidden text**

Sunday, May 24, 2026

Fixed mudlet UI capability

I successfully built mudlet using the FreeBSD ports tree framework and mechanisms.  It installed and ran, but what I didn't know until after I was trying to play on a mud server, was that some capablity was missing.  What I saw was not very friendly, and I fought with the mapping system to try to understand that as well.  The mud showed me its fancy ansi colors and the mapping mechanism allowed me to use the numpad to move in all those cardinal directions.  What I saw then was nothing compared to what I see now and the function mudlet has for me now.

Above is what I gained, but to get there I went back to hacking on the build of mudlet to see if anything I did was the reason the interface was lacking and different scripts gave errors instead of function.  I had to figure out why all that I could see was what I discovered to be a broken UI.  I had to figure out what was actually wrong.

That grey box I could cause to be hidden, and I could use the map that was present but I didn't know how to create more maps or if I could.  I fought with mudlet while playing on the server anyway, knowing something was incomplete or missing.  I went through my Makefile and switched things from luarocks provided over to obtained by ports, I changed numerous things in the Makefile, back and forth, and either I caused more scripts to show as a bug or the same one.

I saw the error "Lua syntax error:...hare/mudlet/lua/geyser/GeyserAdjustableContainer.lua:609: attempt to index field 'Locale' (a nil value)" and put all focus on one word, Locale.  I believed it was related to localization but it turned out that after all effort to cure that issue, I was wrong.  After more banging my head against a brick wall, I decided that maybe a code checker site for lua would help me.  I don't know lua, but possibly the issue really is a syntax error as the message says.  I found a site that helped me, with AI, and gave plenty of instruction about why each bit that it fixed was not best practice or apt to cause the nil value error.

I used the corrected code result the site provided to me as the revised version of the file GeyserAdjustableContainer.lua and used GeyserAdjustableContainer.lua.orig as the untouched original file, generated a massive patch with make makepatch and then cleaned up to reinstall.

--- src/mudlet-lua/lua/geyser/GeyserAdjustableContainer.lua.orig	2026-05-14 17:24:18 UTC
+++ src/mudlet-lua/lua/geyser/GeyserAdjustableContainer.lua
@@ -10,30 +10,44 @@ Adjustable = Adjustable or {}
 
 Adjustable = Adjustable or {}
 
-Adjustable.Container = Adjustable.Container or Geyser.Container:new({name = "AdjustableContainerClass"})
+Adjustable.Container = Adjustable.Container or Geyser.Container:new({
+    name = "AdjustableContainerClass",
+    padding = 10,
+    buttonsize = 20,
+    adjLabelstyle = "background-color:rgba(0,0,0,100); border: 1px solid grey;",
+    titleTxtColor = "green",
+    titleFormat = "l",
+    attachedMargin = 0,
+})
 
-local adjustInfo = {}
+-- Static/Default Locale Table
+Adjustable.Container.Locale = Adjustable.Container.Locale or {
+    connectTo = { message = "Connect To: " },
+    disconnect = { message = "Disconnect " },
+    top = { message = "Top" },
+    bottom = { message = "Bottom" },
+    left = { message = "Left" },
+    right = { message = "Right" }
+}
 
+-- Registry for attached containers
+Adjustable.Container.Attached = Adjustable.Container.Attached or {
+    top = {}, bottom = {}, left = {}, right = {}
+}
+
 -- Internal function to add "%" to a value and round it
--- Resulting percentage has five precision points to ensure accurate 
--- representation in pixel space.
--- @param num Any float.  For 0-100% output, use 0.0-1.0
 local function make_percent(num)
     return string.format("%.5f%%", (num * 100))
 end
 
--- Internal function: checks where the mouse is at on the Label
--- and saves the information for further use at resizing/repositioning
--- also changes the mousecursor for easier use of the resizing/repositioning functionality
--- @param self the Adjustable.Container it self
--- @param label the Label which allows the Container to be adjustable
--- @param event Mouse Click event and its infomations
+-- Internal function: checks mouse position and sets state
 local function adjust_Info(self, label, event)
     local x, y = getMousePosition()
     local w, h = self.adjLabel:get_width(), self.adjLabel:get_height()
     local x1, y1 = x - event.x, y - event.y
     local x2, y2 = x1 + w, y1 + h
     local left, right, top, bottom = event.x <= 10, x >= x2 - 10, event.y <= 3, y >= y2 - 10
+    
     if right and left then left = false end
     if top and bottom then top = false end
 
@@ -51,25 +65,29 @@ local function adjust_Info(self, label, event)
         end
     end
 
-    adjustInfo = {name = adjustInfo.name, top = top, bottom = bottom, left = left, right = right, x = x, y = y, move = adjustInfo.move}
+    self.adjustInfo = {
+        name = self.name, 
+        top = top, 
+        bottom = bottom, 
+        left = left, 
+        right = right, 
+        x = x, 
+        y = y, 
+        move = (self.adjustInfo and self.adjustInfo.move)
+    }
 end
 
---- function to give your adjustable container a new title
--- @param text new title text
--- @param color title text color
--- @param format A format list to use.  'c' - center, 'l' - left, 'r' - right,  'b' - bold, 'i' - italics, 'u' - underline, 's' - strikethrough,  '##' - font size.   For example, "cb18" specifies center bold 18pt font be used.   Order doesn't matter.
 function Adjustable.Container:setTitle(text, color, format)
     self.titleFormat = format or self.titleFormat or "l"
-    self.titleText = text or self.titleText or string.format("%s - Adjustable Container")
+    self.titleText = text or self.titleText or string.format("%s - Adjustable Container", self.name)
     self.titleTxtColor = color or self.titleTxtColor or "green"
+    
     if self.locked and (self.connectedContainers or self.lockStyle == "standard" or self.lockStyle == "border" or self.lockStyle == "full") then
         return
     end
     self.adjLabel:echo(string.format("  %s", self.titleText), self.titleTxtColor, self.titleFormat)
 end
 
-
---- function to reset your adjustable containers title to default
 function Adjustable.Container:resetTitle()
     self.titleText = nil
     self.titleTxtColor = nil
@@ -77,65 +95,58 @@ end
     self:setTitle()
 end
 
--- internal function to handle the onClick event of main Adjustable.Container Label
--- @param label the main Adjustable.Container Label
--- @param event the onClick event and its information
 function Adjustable.Container:onClick(label, event)
+    self.adjustInfo = self.adjustInfo or {}
     if label.cursorShape == "OpenHand" then
         label:setCursor("ClosedHand")
     end
-    if event.button == "LeftButton" and not(self.locked and not self.connectedContainers) then
+    
+    if event.button == "LeftButton" and not (self.locked and not self.connectedContainers) then
         if self.raiseOnClick then
             self:raiseAll()
         end
-        adjustInfo.name = label.name
-        adjustInfo.move = not (adjustInfo.right or adjustInfo.left or adjustInfo.top or adjustInfo.bottom)
-        if self.minimized then adjustInfo.move = true end
+        self.adjustInfo.name = label.name
+        self.adjustInfo.move = not (self.adjustInfo.right or self.adjustInfo.left or self.adjustInfo.top or self.adjustInfo.bottom)
+        if self.minimized then self.adjustInfo.move = true end
         adjust_Info(self, label, event)
     end
+    
     if event.button == "RightButton" then
-        --if not in the Geyser main window attach Label is not needed and will be removed
-        if self.container ~= Geyser and table.index_of(self.rCLabel.nestedLabels, self.attLabel) then
+        if self.container ~= Geyser and self.rCLabel and table.index_of(self.rCLabel.nestedLabels or {}, self.attLabel) then
             label:hideMenuLabel("attLabel")
-            -- if we are back to the Geyser main window attach Label will be re-added
-        elseif self.container == Geyser and not table.index_of(self.rCLabel.nestedLabels, self.attLabel) then
+        elseif self.container == Geyser and self.rCLabel and not table.index_of(self.rCLabel.nestedLabels or {}, self.attLabel) then
             label:showMenuLabel("attLabel") 
         end
 
-        if not self.customItemsLabel.nestedLabels then
+        if self.customItemsLabel and (not self.customItemsLabel.nestedLabels or #self.customItemsLabel.nestedLabels == 0) then
             label:hideMenuLabel("customItemsLabel")
-        else
+        elseif self.customItemsLabel then
             label:showMenuLabel("customItemsLabel")
         end
     end
-    label:onRightClick(event)
+    
+    if label.onRightClick then label:onRightClick(event) end
 end
 
--- internal function to handle the onRelease event of main Adjustable.Container Label
---- raises an event "AdjustableContainerRepositionFinish", passed values (name, width, height, x, y)
--- @param label the main Adjustable.Container Label
--- @param event the onRelease event and its information
-function Adjustable.Container:onRelease (label, event)
-    if event.button == "LeftButton" and adjustInfo ~= {} and adjustInfo.name == label.name then
+function Adjustable.Container:onRelease(label, event)
+    if event.button == "LeftButton" and self.adjustInfo and self.adjustInfo.name == label.name then
         if label.cursorShape == "ClosedHand" then
             label:setCursor("OpenHand")
         end
         raiseEvent(
           "AdjustableContainerRepositionFinish",
           self.name,
-          self.get_width(),
-          self.get_height(),
-          self.get_x(),
-          self.get_y()
+          self:get_width(),
+          self:get_height(),
+          self:get_x(),
+          self:get_y()
         )
-        adjustInfo = {}
+        self.adjustInfo = {}
     end
 end
 
--- internal function to handle the onMove event of main Adjustable.Container Label
--- @param label the main Adjustable.Container Label
--- @param event the onMove event and its information
-function Adjustable.Container:onMove (label, event)
+function Adjustable.Container:onMove(label, event)
+    self.adjustInfo = self.adjustInfo or {}
     if self.locked and not self.connectedContainers then
         if label.cursorShape ~= 0 then
             label:resetCursor()
@@ -143,236 +154,182 @@ function Adjustable.Container:onMove (label, event)
         return
     end
     
-    if adjustInfo.move == nil then
+    if self.adjustInfo.move == nil then
         adjust_Info(self, label, event)
     end
 
     if self.connectedToBorder then
         for k in pairs(self.connectedToBorder) do
-            if adjustInfo[k] then
+            if self.adjustInfo[k] then
                 label:resetCursor()
                 return
             end
         end
     end
 
-    if adjustInfo.x and adjustInfo.name == label.name then
+    if self.adjustInfo.x and self.adjustInfo.name == label.name then
         self:adjustBorder()
         local x, y = getMousePosition()
         local winw, winh = getMainWindowSize()
-        local x1, y1, w, h = self.get_x(), self.get_y(), self:get_width(), self:get_height()
+        local x1, y1, w, h = self:get_x(), self:get_y(), self:get_width(), self:get_height()
+        
         if (self.container) and (self.container ~= Geyser) then
-            x1,y1 = x1-self.container.get_x(), y1-self.container.get_y()
-            winw, winh = self.container.get_width(), self.container.get_height()
+            x1, y1 = x1 - self.container:get_x(), y1 - self.container:get_y()
+            winw, winh = self.container:get_width(), self.container:get_height()
         end
-        local dx, dy = adjustInfo.x - x, adjustInfo.y - y
+        
+        local dx, dy = self.adjustInfo.x - x, self.adjustInfo.y - y
         local max, min = math.max, math.min
         local hasScrollBox = self.windowname and Geyser.parentWindows and Geyser.parentWindows[self.windowname] and Geyser.parentWindows[self.windowname].type == "scrollBox"
-        if adjustInfo.move and not self.connectedContainers then
+        
+        if self.adjustInfo.move and not self.connectedContainers then
             label:setCursor("ClosedHand")
-            local tx, ty = max(0,x1-dx), max(0,y1-dy)
-            -- get rid of move/size limits when in scrollbox (as it is scrollable)
-            if not(hasScrollBox) then
+            local tx, ty = max(0, x1 - dx), max(0, y1 - dy)
+            if not hasScrollBox then
                 tx, ty = min(tx, winw - w), min(ty, winh - h)
             end
-            tx = make_percent(tx/winw)
-            ty = make_percent(ty/winh)
-            self:move(tx, ty)
-            --[[
-            -- automated lock on border deactivated for now
-            if x1-dx <-5 then self:attachToBorder("left") end
-            if y1-dy <-5 then self:attachToBorder("top") end
-            if winw - w < tx+0.1 then self:attachToBorder("right") end
-            if winh - h < ty+0.1 then self:attachToBorder("bottom") end--]]
-        elseif adjustInfo.move == false then
+            self:move(make_percent(tx / winw), make_percent(ty / winh))
+        elseif self.adjustInfo.move == false then
             local w2, h2, x2, y2 = w - dx, h - dy, x1 - dx, y1 - dy
             local tx, ty, tw, th = x1, y1, w, h
-            if adjustInfo.top then
+            if self.adjustInfo.top then
                 ty, th = y2, h + dy
-            elseif adjustInfo.bottom then
+            elseif self.adjustInfo.bottom then
                 th = h2
             end
-            if adjustInfo.left then
+            if self.adjustInfo.left then
                 tx, tw = x2, w + dx
-            elseif adjustInfo.right then
+            elseif self.adjustInfo.right then
                 tw = w2
             end
-            tx, ty, tw, th = max(0,tx), max(0,ty), max(10,tw), max(10,th)
-            if not(hasScrollBox) then
+            tx, ty, tw, th = max(0, tx), max(0, ty), max(10, tw), max(10, th)
+            if not hasScrollBox then
                 tw, th = min(tw, winw), min(th, winh)
-                tx, ty = min(tx, winw-tw), min(ty, winh-th)
+                tx, ty = min(tx, winw - tw), min(ty, winh - th)
             end
-            tx = make_percent(tx/winw)
-            ty = make_percent(ty/winh)
-            self:move(tx, ty)
-            local minw, minh = 0,0
-            if self.container == Geyser and not self.noLimit then minw, minh = 75,25 end
-            tw,th = max(minw,tw), max(minh,th)
-            tw,th = make_percent(tw/winw), make_percent(th/winh)
-            self:resize(tw, th)
+            self:move(make_percent(tx / winw), make_percent(ty / winh))
+            
+            local minw, minh = 0, 0
+            if self.container == Geyser and not self.noLimit then minw, minh = 75, 25 end
+            tw, th = max(minw, tw), max(minh, th)
+            self:resize(make_percent(tw / winw), make_percent(th / winh))
+            
             if self.connectedContainers then
                 self:adjustConnectedContainers()
             end
         end
-        adjustInfo.x, adjustInfo.y = x, y
+        self.adjustInfo.x, self.adjustInfo.y = x, y
     end
 end
 
--- internal function to check which valid attach position the container is at
 function Adjustable.Container:validAttachPositions()
     local winw, winh = getMainWindowSize()
     local found_positions = {}
-    if  (winh*0.8)-self.get_height()<= self.get_y()  then  found_positions[#found_positions+1] = "bottom" end
-    if  (winw*0.8)-self.get_width() <= self.get_x() then  found_positions[#found_positions+1] = "right" end
-    if self.get_y() <= winh*0.2 then found_positions[#found_positions+1] = "top" end
-    if self.get_x() <= winw*0.2 then found_positions[#found_positions+1] = "left" end
+    if (winh * 0.8) - self:get_height() <= self:get_y() then found_positions[#found_positions+1] = "bottom" end
+    if (winw * 0.8) - self:get_width() <= self:get_x() then found_positions[#found_positions+1] = "right" end
+    if self:get_y() <= winh * 0.2 then found_positions[#found_positions+1] = "top" end
+    if self:get_x() <= winw * 0.2 then found_positions[#found_positions+1] = "left" end
     return found_positions
 end
 
--- internal function to adjust the main console borders if needed
 function Adjustable.Container:adjustBorder()
     local winw, winh = getMainWindowSize()
-    local where = false
+    if type(self.attached) ~= "string" then return false end
 
-    if type(self.attached) ~= "string" then
-        return false
-    end
-
-    where = self.attached:lower()
-    if table.contains(self:validAttachPositions(), where) == false or self.minimized or self.hidden then 
+    local where = self.attached:lower()
+    local valid = self:validAttachPositions()
+    if not table.contains(valid, where) or self.minimized or self.hidden then 
         self:detach()
         return
     end
 
-    if  where == "right" then 
-        self.borderSize = winw+self.attachedMargin-self.get_x()
-    elseif  where == "left"    then
-        self.borderSize =  self.get_width()+self.get_x()+self.attachedMargin
-    elseif  where == "bottom"  then 
-        self.borderSize = winh+self.attachedMargin-self.get_y()
-    elseif  where == "top"     then 
-        self.borderSize = self.get_height()+self.get_y()+self.attachedMargin
+    if where == "right" then 
+        self.borderSize = winw + self.attachedMargin - self:get_x()
+    elseif where == "left" then
+        self.borderSize = self:get_width() + self:get_x() + self.attachedMargin
+    elseif where == "bottom" then 
+        self.borderSize = winh + self.attachedMargin - self:get_y()
+    elseif where == "top" then 
+        self.borderSize = self:get_height() + self:get_y() + self.attachedMargin
     else
         self.attached = false
         return
     end
+
     local borderSize = self.borderSize
-    for k,v in pairs(Adjustable.Container.Attached[where]) do
-        if v.borderSize > borderSize then
+    for k, v in pairs(Adjustable.Container.Attached[where] or {}) do
+        if v.borderSize and v.borderSize > borderSize then
             borderSize = v.borderSize
         end
     end
-    local funcname = string.format("setBorder%s", string.title(where))
-    _G[funcname](borderSize)
+    
+    local funcname = string.format("setBorder%s", where:gsub("^%l", string.upper))
+    if _G[funcname] then _G[funcname](borderSize) end
 end
 
--- internal function to adjust connected containers
 function Adjustable.Container:adjustConnectedContainers()
     local where = self.attached
-    local x, y, height, width = self.x, self.y, self.height, self.width
-    if not where or not self.connectedContainers then
-        return false
-    end
-    for k in pairs(self.connectedContainers) do
-        local container = Adjustable.Container.all[k]
-        if container then
-            if container.attached == where then
-                if where == "right" or where == "left" then
-                    height = nil
-                    y = nil
-                end
-                if where == "top" or where == "bottom" then
-                    width = nil
-                    x = nil
-                end
-                container:move(x, y)
-                container:resize(width, height)
-            else
-                if where == "right" then
-                    container:resize(self:get_x() - container:get_x(), nil)
-                end
-                if where == "left" then
-                    local right_x = container:get_x() + container:get_width()
-                    local left_x = self:get_x() + self:get_width()
-                    container:move(left_x, nil)
-                    container:resize(right_x - container:get_x(), nil)
-                end
-                if where == "bottom" then
-                    container:resize(nil, self:get_y() - container:get_y())
-                end
-                if where == "top" then
-                    local bottom_y = container:get_y() + container:get_height()
-                    local top_y = self:get_y() + self:get_height()
-                    container:move(nil, top_y)
-                    container:resize(nil, bottom_y - container:get_y())
-                end
-            end
+    if not where or not self.connectedContainers then return false end
+    -- Implementation of container shifting based on parent movement
+    for k, _ in pairs(self.connectedContainers) do
+        local container = Adjustable.Container.Attached[where][k]
+        if container and container ~= self then
             container:adjustBorder()
         end
     end
 end
 
---- connect your container to a border
--- @param border main border ("top", "bottom", "left", "right")
 function Adjustable.Container:connectToBorder(border)
-    if not self.attached or not Adjustable.Container.Attached[border] then
-        return
-    end
+    if not self.attached or not Adjustable.Container.Attached[border] then return end
     self.connectedToBorder = self.connectedToBorder or {}
     self.connectedToBorder[border] = true
     self.connectedContainers = self.connectedContainers or {}
-    for k,v in pairs(Adjustable.Container.Attached[border]) do
+    for k, v in pairs(Adjustable.Container.Attached[border]) do
         v.connectedContainers = v.connectedContainers or {}
         v.connectedContainers[self.name] = true
         if self.attached == border then
             v.connectedToBorder = v.connectedToBorder or {}
             v.connectedToBorder[border] = true
-            self.connectedContainers[k] = v
+            self.connectedContainers[k] = true
         end
-        v:adjustConnectedContainers()
     end
 end
 
---- adds elements to connect containers to borders into the right click menu
 function Adjustable.Container:addConnectMenu()
     local label = self.adjLabel
-    -- Check if menu already exists to prevent duplicates when called multiple times
-    if label:findMenuElement("Connect To: ") then
-        return
-    end
+    if not label or label:findMenuElement("Connect To: ") then return end
+    
     local menuTxt = self.Locale.connectTo.message
     label:addMenuLabel("Connect To: ")
     label:findMenuElement("Connect To: "):echo(menuTxt, "nocolor", "c")
+    
     local menuParent = self.rCLabel.MenuItems
     menuParent[#menuParent + 1] = {"top", "bottom", "left", "right"}
-    self.rCLabel.MenuWidth3 = self.ChildMenuWidth
-    self.rCLabel.MenuFormat3 = self.rCLabel.MenuFormat2
+    
     label:createMenuItems()
-    for  k,v in ipairs(menuParent[#menuParent]) do
-        menuTxt = self.Locale[v] and self.Locale[v].message or v
-        label:findMenuElement("Connect To: ."..v):echo(menuTxt, "nocolor")
-        label:setMenuAction("Connect To: ."..v, function() closeAllLevels(self.rCLabel) self:connectToBorder(v) end)
+    for _, v in ipairs(menuParent[#menuParent]) do
+        local subTxt = self.Locale[v] and self.Locale[v].message or v
+        label:findMenuElement("Connect To: ."..v):echo(subTxt, "nocolor")
+        label:setMenuAction("Connect To: ."..v, function() 
+            if closeAllLevels then closeAllLevels(self.rCLabel) end
+            self:connectToBorder(v) 
+        end)
     end
-    menuTxt = self.Locale.disconnect.message
+    
     label:addMenuLabel("Disconnect ")
-    label:setMenuAction("Disconnect ", function() closeAllLevels(self.rCLabel) self:disconnect() end)
-    label:findMenuElement("Disconnect "):echo(menuTxt, "nocolor", "c")
+    label:setMenuAction("Disconnect ", function() 
+        if closeAllLevels then closeAllLevels(self.rCLabel) end
+        self:disconnect() 
+    end)
+    label:findMenuElement("Disconnect "):echo(self.Locale.disconnect.message, "nocolor", "c")
 end
 
---- disconnects your container from a border
 function Adjustable.Container:disconnect()
-    if not self.connectedToBorder then
-        return
-    end
-    for k in pairs(self.connectedToBorder) do
-        if Adjustable.Container.Attached[k] then
-            for k1,v1 in pairs(Adjustable.Container.Attached[k]) do
-                if v1.connectedContainers and v1.connectedContainers[self.name] then
-                    v1.connectedContainers[self.name] = nil
-                    if table.is_empty(v1.connectedContainers) then
-                        v1.connectedContainers = nil
-                    end
-                end
+    if not self.connectedToBorder then return end
+    for border, _ in pairs(self.connectedToBorder) do
+        for _, v1 in pairs(Adjustable.Container.Attached[border] or {}) do
+            if v1.connectedContainers then
+                v1.connectedContainers[self.name] = nil
             end
         end
     end
@@ -380,808 +337,135 @@ end
     self.connectedContainers = nil
 end
 
---- gives your MainWindow borders a margin
--- @param margin in pixel
 function Adjustable.Container:setBorderMargin(margin)
     self.attachedMargin = margin
     self:adjustBorder()
 end
 
--- internal function to resize the border automatically if the window size changes
 function Adjustable.Container:resizeBorder()
     local winw, winh = getMainWindowSize()
-    self.timer_active = self.timer_active or true
-    -- Check if Window resize already happened.
-    -- If that is not checked this creates an infinite loop and crashes because setBorder also causes a resize event
-    if (winw ~= self.old_w_value or winh ~= self.old_h_value) and self.timer_active then
-        self.timer_active = false
-        tempTimer(0.2, function() self:adjustBorder() self:adjustConnectedContainers() end)
+    if (winw ~= self.old_w_value or winh ~= self.old_h_value) then
+        if not self.timer_active then
+            self.timer_active = true
+            tempTimer(0.2, function() 
+                self:adjustBorder() 
+                self:adjustConnectedContainers() 
+                self.timer_active = false
+            end)
+        end
     end
-    self.old_w_value = winw
-    self.old_h_value = winh
+    self.old_w_value, self.old_h_value = winw, winh
 end
 
---- attaches your container to the given border
--- attach is only possible if the container is located near the border
--- @param border possible border values are "top", "bottom", "right", "left"
 function Adjustable.Container:attachToBorder(border)
     if self.attached then self:detach() end
     Adjustable.Container.Attached[border] = Adjustable.Container.Attached[border] or {}
     Adjustable.Container.Attached[border][self.name] = self
     self.attached = border
     self:adjustBorder()
-    self.resizeHandlerID=registerAnonymousEventHandler("sysWindowResizeEvent", function() self:resizeBorder() end)
-    closeAllLevels(self.rCLabel)
+    self.resizeHandlerID = registerAnonymousEventHandler("sysWindowResizeEvent", function() self:resizeBorder() end)
 end
 
---- detaches the given container
--- this means the mudlet main window border will be reset
 function Adjustable.Container:detach()
-    if Adjustable.Container.Attached and Adjustable.Container.Attached[self.attached] then
+    if self.attached and Adjustable.Container.Attached[self.attached] then
         Adjustable.Container.Attached[self.attached][self.name] = nil
+        self:resetBorder(self.attached)
     end
     self.borderSize = nil
-    self:resetBorder(self.attached)
-    self.attached=false
+    self.attached = false
     if self.resizeHandlerID then killAnonymousEventHandler(self.resizeHandlerID) end
 end
 
--- internal function to reset the given border
--- @param where possible border values are "top", "bottom", "right", "left"
 function Adjustable.Container:resetBorder(where)
     local resetTo = 0
-    if not Adjustable.Container.Attached[where] then
-        return
-    end
-    for k,v in pairs(Adjustable.Container.Attached[where]) do
-        if v.borderSize > resetTo then
+    for _, v in pairs(Adjustable.Container.Attached[where] or {}) do
+        if v.borderSize and v.borderSize > resetTo then
             resetTo = v.borderSize
         end
     end
-    if        where == "right"   then setBorderRight(resetTo)
-    elseif  where == "left"    then setBorderLeft(resetTo)
-    elseif  where == "bottom"  then setBorderBottom(resetTo)
-    elseif  where == "top"     then setBorderTop(resetTo)
-    end
+    local func = _G["setBorder" ..  where:gsub("^%l", string.upper)]
+    if func then func(resetTo) end
 end
 
--- creates the adjustable label and the container where all the elements will be put in
 function Adjustable.Container:createContainers()
     self.adjLabel = Geyser.Label:new({
-        x = "0",
-        y = "0",
-        height = "100%",
-        width = "100%",
+        x = 0, y = 0, height = "100%", width = "100%",
         name = self.name.."adjLabel"
-    },self)
+    }, self)
+    self.adjLabel:setStyleSheet(self.adjLabelstyle)
+    
     self.Inside = Geyser.Container:new({
-        x = self.padding,
-        y = self.padding*2,
-        height = "-"..self.padding,
-        width = "-"..self.padding,
+        x = self.padding, y = self.padding * 2,
+        height = "-" ..  self.padding, width = "-" ..  self.padding,
         name = self.name.."InsideContainer"
-    },self)
+    }, self)
 end
 
---- locks your adjustable container
---lock means that your container is no longer moveable/resizable by mouse.  
---You can also choose different lockStyles which changes the border or container style.  
---if no lockStyle is added "standard" style will be used 
--- @param lockNr the number of the lockStyle [optional]
--- @param lockStyle the lockstyle used to lock the container, 
--- the lockStyle is the behaviour/mode of the locked state.
--- integrated lockStyles are "standard", "border", "full" and "light" (default "standard")
--- standard:    This is the default lockstyle, with a small margin on top to keep the right click menu usable.
--- light:       Only hides the min/restore and close labels.  Borders and margin are not affected.
--- full:        The container gets fully locked without any margin left for the right click menu.
--- border:      Keeps the borders of the container visible while locked.
-
 function Adjustable.Container:lockContainer(lockNr, lockStyle)
-    closeAllLevels(self.rCLabel)
-
-    if type(lockNr) == "string" then
-      lockStyle = lockNr
-    elseif type(lockNr) == "number" then
-      lockStyle = self.lockStyles[lockNr][1]
-    end
-
-    lockStyle = lockStyle or self.lockStyle
-    if not self.lockStyles[lockStyle] then
-      lockStyle = "standard"
-    end
-
-    self.lockStyle = lockStyle
-
-    if self.minimized == false then
-        self.lockStyles[lockStyle][2](self)
-        self.exitLabel:hide()
-        self.minimizeLabel:hide()
-        self.locked = true
-        self:adjustBorder()
-    end
+    if type(lockNr) == "string" then lockStyle = lockNr end
+    self.lockStyle = lockStyle or "standard"
+    self.locked = true
+    self.exitLabel:hide()
+    self.minimizeLabel:hide()
+    self:adjustBorder()
 end
 
--- internal function to handle the custom Items onClick event
--- @param customItem the item clicked at
-function Adjustable.Container:customMenu(customItem)
-    closeAllLevels(self.rCLabel)
-    if self.minimized == false then
-        self.customItems[customItem][2](self)
-    end
-end
-
---- unlocks your previous locked container
--- what means that the container is moveable/resizable by mouse again 
 function Adjustable.Container:unlockContainer()
-    closeAllLevels(self.rCLabel)
-    self.Inside:resize("-"..self.padding,"-"..self.padding)
+    self.locked = false
+    self.Inside:resize("-"..self.padding, "-"..self.padding)
     self.Inside:move(self.padding, self.padding*2)
-    self.adjLabel:setStyleSheet(self.adjLabelstyle)
     self.exitLabel:show()
     self.minimizeLabel:show()
-    self.locked = false
     self:setTitle()
 end
 
---- sets the padding of your container
--- changes how far the the container is positioned from the border of the container 
--- padding behaviour also depends on your lockStyle
--- @param padding the padding value (standard is 10)
-function Adjustable.Container:setPadding(padding)
-    self.padding = padding
-    if self.locked then
-        self:lockContainer()
-    else
-        self:unlockContainer()
-    end
-end
-
--- internal function: onClick Lock event
-function Adjustable.Container:onClickL()
-    if self.locked == true then
-        self:unlockContainer()
-    else
-        self:lockContainer()
-    end
-end
-
--- internal function: adjusts/sets the borders if an container gets hidden
-function Adjustable.Container:hideObj()
-    self:hide()
-    self:adjustBorder()
-end
-
--- internal function: onClick minimize event
-function Adjustable.Container:onClickMin()
-    closeAllLevels(self.rCLabel)
-    if self.minimized == false then
-        self:minimize()
-    else
-        self:restore()
-    end
-end
-
--- internal function: onClick save event
-function Adjustable.Container:onClickSave()
-    closeAllLevels(self.rCLabel)
-    self:save()
-end
-
--- internal function: onClick load event
-function Adjustable.Container:onClickLoad()
-    closeAllLevels(self.rCLabel)
-    self:load()
-end
-
---- minimizes the container
--- hides everything beside the title
 function Adjustable.Container:minimize()
-    if self.minimized and self.locked then
-        return
-    end
+    if self.minimized then return end
     self.origh = self.height
     self.Inside:hide()
     self:resize(nil, self.buttonsize + 10)
     self.minimized = true
-    if self.connectedToBorder or self.connectedContainers then
-        self:disconnect()
-    end
     self:adjustBorder()
 end
 
---- restores the container after it was minimized
 function Adjustable.Container:restore()
-    if self.minimized == true then
-        self.origh = self.origh or "25%"
-        self.Inside:show()
-        self:resize(nil,self.origh)
-        self.minimized = false
-        self:adjustBorder()
-    end
-end
-
--- internal function to create the menu labels for lockstyle and custom items
--- @param self the container itself
--- @param menu name of the menu
--- @param onClick function which will be executed onClick
-local function createMenus(self, parent, name, func)
-    local label = self.adjLabel
-    local menuTxt = self.Locale[name] and self.Locale[name].message or name
-    label:addMenuLabel(name, parent)
-    label:findMenuElement(parent.."."..name):echo(menuTxt, "nocolor")
-    label:setMenuAction(parent.."."..name, func, self, name)
-end
-
--- internal function: Handler for the onEnter event of the attach menu
--- the attach menu will be created with the valid positions onEnter of the mouse
-function Adjustable.Container:onEnterAtt()
-    local attm = self:validAttachPositions()
-    self.attLabel.nestedLabels = {}
-    for i=1,#attm do
-        if self.att[i].container ~= Geyser then
-            self.att[i]:changeContainer(Geyser)
-        end
-        self.att[i].flyDir = self.attLabel.flyDir
-        self.att[i]:echo("
"..self.Locale[attm[i]].message, "nocolor") - self.att[i]:setClickCallback("Adjustable.Container.attachToBorder", self, attm[i]) - self.attLabel.nestedLabels[#self.attLabel.nestedLabels+1] = self.att[i] - end - doNestShow(self.attLabel) -end - --- internal function to create the Minimize/Close and the right click Menu Labels -function Adjustable.Container:createLabels() - self.exitLabel = Geyser.Label:new({ - x = -(self.buttonsize * 1.4), y=4, width = self.buttonsize, height = self.buttonsize, fontSize = self.buttonFontSize, name = self.name.."exitLabel" - - },self) - self.exitLabel:echo("
x
") - - - self.minimizeLabel = Geyser.Label:new({ - x = -(self.buttonsize * 2.6), y=4, width = self.buttonsize, height = self.buttonsize, fontSize = self.buttonFontSize, name = self.name.."minimizeLabel" - - },self) - self.minimizeLabel:echo("
-
") -end - --- internal function to create the right click menu -function Adjustable.Container:createRightClickMenu() - self.adjLabel:createRightClickMenu( - {MenuItems = {"lockLabel", "minLabel", "saveLabel", "loadLabel", "attLabel", {"att1","att2","att3","att4"}, "lockStylesLabel",{}, "customItemsLabel",{}}, - Style = self.menuStyleMode, - MenuStyle = self.menustyle, - MenuWidth = self.ParentMenuWidth, - MenuWidth2 = self.ChildMenuWidth, - MenuHeight = self.MenuHeight, - MenuFormat = "l"..self.MenuFontSize, - MenuFormat2 = "c"..self.MenuFontSize, - } - ) - self.rCLabel = self.adjLabel.rightClickMenu - for k,v in pairs(self.rCLabel.MenuLabels) do - self[k] = v - end - for k,v in ipairs(self.rCLabel.MenuLabels["attLabel"].MenuItems) do - self.att[k] = self.rCLabel.MenuLabels["attLabel"].MenuLabels[v] - end -end - --- internal function to set the text on the right click menu labels -function Adjustable.Container:echoRightClickMenu() - for k,v in ipairs(self.adjLabel.rightClickMenu.MenuItems) do - if type(v) == "string" then - self[v]:echo(self[v].txt, "nocolor") - end - end -end - ---- function to change the right click menu style --- there are 2 styles: dark and light ---@param mode the style mode (dark or light) -function Adjustable.Container:changeMenuStyle(mode) - self.menuStyleMode = mode - self.adjLabel:styleMenuItems(self.menuStyleMode) -end - --- overridden add function to put every new window to the Inside container --- @param window derives from the original Geyser.Container:add function --- @param cons derives from the original Geyser.Container:add function -function Adjustable.Container:add(window, cons) - if self.goInside then - if self.useAdd2 == false then - self.Inside:add(window, cons) - else - --add2 inheritance set to true - self.Inside:add2(window, cons, true) - end - else - if self.useAdd2 == false then - Geyser.add(self, window, cons) - else - --add2 inheritance set to true - self:add2(window, cons, true) - end - end -end - --- overridden show function to prevent to show the right click menu on show -function Adjustable.Container:show(auto) - Geyser.Container.show(self, auto) - closeAllLevels(self.rCLabel) -end - ---- saves your container settings --- like position/size and some other variables in your Mudlet Profile Dir/ AdjustableContainer --- to be reliable it is important that the Adjustable.Container has an unique 'name' --- @param slot defines a save slot for example a number (1,2,3..) or a string "backup" [optional] --- @param dir defines save directory [optional] --- @see Adjustable.Container:load -function Adjustable.Container:save(slot, dir) - assert(slot == nil or type(slot) == "string" or type(slot) == "number", "Adjustable.Container.save: bad argument #1 type (slot as string or number expected, got "..type(slot).."!)") - assert(dir == nil or type(dir) == "string" , "Adjustable.Container.save: bad argument #2 type (directory as string expected, got "..type(dir).."!)") - dir = dir or self.defaultDir - local saveDir = string.format("%s%s.lua", dir, self.name) - local mainTable = {} - mainTable.slot = {} - local mytable = {} - - -- check if there are already saved settings and if so load them to the mainTable - if io.exists(saveDir) then - table.load(saveDir, mainTable) - end - - if slot then - mainTable.slot[slot] = mytable - else - mytable = mainTable - end - - mytable.x = self.x - mytable.y = self.y - mytable.height= self.height - mytable.width= self.width - mytable.minimized= self.minimized - mytable.origh= self.origh - mytable.locked = self.locked - mytable.attached = self.attached - mytable.lockStyle = self.lockStyle - mytable.padding = self.padding - mytable.attachedMargin = self.attachedMargin - mytable.hidden = self.hidden - mytable.auto_hidden = self.auto_hidden - mytable.connectedToBorder = self.connectedToBorder - mytable.connectedContainers = self.connectedContainers - mytable.windowname = self.windowname - if not(io.exists(dir)) then lfs.mkdir(dir) end - table.save(saveDir, mainTable) - return true -end - ---- restores/loads the before saved settings --- @param slot defines a load slot for example a number (1,2,3..) or a string "backup" [optional] --- @param dir defines load directory [optional] --- @see Adjustable.Container:save -function Adjustable.Container:load(slot, dir) - local mytable = {} - mytable.slot = {} - assert(slot == nil or type(slot) == "string" or type(slot) == "number", "Adjustable.Container.load: bad argument #1 type (slot as string or number expected, got "..type(slot).."!)") - assert(dir == nil or type(dir) == "string" , "Adjustable.Container.load: bad argument #2 type (directory as string expected, got "..type(dir).."!)") - dir = dir or self.defaultDir - local loadDir = string.format("%s%s.lua", dir, self.name) - if not (io.exists(loadDir)) then - return string.format("Adjustable.Container.load: Couldn't load settings from %s", loadDir) - end - - local ok = pcall(table.load, loadDir, mytable) - if not ok then - self:deleteSaveFile() - debugc(string.format("Adjustable.Container.load: Save file %s got corrupted.  It was deleted so everything else can load properly.", loadDir)) - return false - end - - -- if slot settings not found load default settings - if slot then - mytable = mytable.slot[slot] or mytable - end - - mytable.windowname = mytable.windowname or "main" - - -- send Adjustable Container to a UserWindow/ScrollBox if saved there - if mytable.windowname ~= self.windowname then - if mytable.windowname == "main" then - self:changeContainer(Geyser) - else - self:changeContainer(Geyser.parentWindows[mytable.windowname]) - end - end - - self.lockStyle = mytable.lockStyle or self.lockStyle - self.padding = mytable.padding or self.padding - self.attachedMargin = mytable.attachedMargin or self.attachedMargin - - - if mytable.x then - self:move(mytable.x, mytable.y) - self:resize(mytable.width, mytable.height) - self.minimized = mytable.minimized - - if mytable.locked == true then self:lockContainer() else self:unlockContainer() end - - if self.minimized == true then self.Inside:hide() self:resize(nil, self.buttonsize + 10) else self.Inside:show() end - self.origh = mytable.origh - end - - if mytable.auto_hidden or mytable.hidden then - self:hide() - if not mytable.hidden then - self.hidden = false - self.auto_hidden = true - end - else - self:show() - end - - self:detach() - if mytable.attached then - self:attachToBorder(mytable.attached) - end - + if not self.minimized then return end + self.Inside:show() + self:resize(nil, self.origh or "25%") + self.minimized = false self:adjustBorder() - - self.connectedContainers = mytable.connectedContainers or self.connectedContainers - self.connectedToBorder = mytable.connectedToBorder or self.connectedToBorder - if self.connectedToBorder then - for k in pairs(self.connectedToBorder) do - self:connectToBorder(k) - end - end - self:adjustConnectedContainers() - return true end ---- overridden reposition function to raise an "AdjustableContainerReposition" event ---- Event: "AdjustableContainerReposition" passed values (name, width, height, x, y, isMouseAction) ---- (the isMouseAction property is true if the reposition is an effect of user dragging/resizing the window, ---- and false if the reposition event comes as effect of external action, such as resizing of main window) -function Adjustable.Container:reposition() - Geyser.Container.reposition(self) - raiseEvent( - "AdjustableContainerReposition", - self.name, - self.get_width(), - self.get_height(), - self.get_x(), - self.get_y(), - adjustInfo.name == self.adjLabel.name and (adjustInfo.move or adjustInfo.right or adjustInfo.left or adjustInfo.top or adjustInfo.bottom) - ) -end - ---- deletes the file where your saved settings are stored --- @param dir defines directory where the saved file is in [optional] --- @see Adjustable.Container:save -function Adjustable.Container:deleteSaveFile(dir) - assert(dir == nil or type(dir) == "string" , "Adjustable.Container.deleteSaveFile: bad argument #1 type (directory as string expected, got "..type(dir).."!)") - dir = dir or self.defaultDir - local deleteDir = string.format("%s%s.lua", dir, self.name) - if io.exists(deleteDir) then - os.remove(deleteDir) - else - return "Adjustable.Container.deleteSaveFile: Couldn't find file to delete at " ..  deleteDir - end - return true -end - ---- saves all your adjustable containers at once --- @param slot defines a save slot for example a number (1,2,3..) or a string "backup" [optional] --- @param dir defines save directory [optional] --- @see Adjustable.Container:save -function Adjustable.Container:saveAll(slot, dir) - for k,v in pairs(Adjustable.Container.all) do - v:save(slot, dir) - end -end - ---- loads all your adjustable containers at once --- @param slot defines a load slot for example a number (1,2,3..) or a string "backup" [optional] --- @param dir defines load directory [optional] --- @see Adjustable.Container:load -function Adjustable.Container:loadAll(slot, dir) - for k,v in pairs(Adjustable.Container.all) do - v:load(slot, dir) - end -end - ---- shows all your adjustable containers --- @see Adjustable.Container:doAll -function Adjustable.Container:showAll() - for k,v in pairs(Adjustable.Container.all) do - v:show() - end -end - ---- executes the function myfunc which affects all your containers --- @param myfunc function which will be executed at all your containers -function Adjustable.Container:doAll(myfunc) - for k,v in pairs(Adjustable.Container.all) do - myfunc(v) - end -end - ---- changes the values of your container to absolute values --- (standard settings are set values to percentages) --- @param size_as_absolute bool true to have the size as absolute values --- @param position_as_absolute bool true to have the position as absolute values -function Adjustable.Container:setAbsolute(size_as_absolute, position_as_absolute) - if position_as_absolute then - self.x, self.y = self.get_x(), self.get_y() - end - if size_as_absolute then - self.width, self.height = self.get_width(), self.get_height() - end - self:set_constraints(self) -end - ---- changes the values of your container to be percentage values --- only needed if values where set to absolute before --- @param size_as_percent bool true to have the size as percentage values --- @param position_as_percent bool true to have the position as percentage values -function Adjustable.Container:setPercent (size_as_percent, position_as_percent) - local x, y, w, h = self:get_x(), self:get_y(), self:get_width(), self:get_height() - local winw, winh = getMainWindowSize() - if (self.container) and (self.container ~= Geyser) then - x,y = x-self.container.get_x(),y-self.container.get_y() - winw, winh = self.container.get_width(), self.container.get_height() - end - x, y, w, h = make_percent(x/winw), make_percent(y/winh), make_percent(w/winw), make_percent(h/winh) - if size_as_percent then self:resize(w,h) end - if position_as_percent then self:move(x,y) end -end --- Save a reference to our parent constructor -Adjustable.Container.parent = Geyser.Container --- Create table to put every Adjustable.Container in it -Adjustable.Container.all = Adjustable.Container.all or {} -Adjustable.Container.all_windows = Adjustable.Container.all_windows or {} -Adjustable.Container.Attached = Adjustable.Container.Attached or {} - --- Internal function to create all the standard lockstyles -function Adjustable.Container:globalLockStyles() - self.lockStyles = self.lockStyles or {} - self:newLockStyle("standard", function (s) - s.Inside:resize("100%",-1) - s.Inside:move(0, s.padding) - s.adjLabel:setStyleSheet(string.gsub(s.adjLabelstyle, "(border.-)%d(.-;)","%10%2")) - s.adjLabel:echo("") - end) - - self:newLockStyle("border", function (s) - s.Inside:resize("-"..s.padding,"-"..s.padding) - s.Inside:move(s.padding, s.padding) - s.adjLabel:setStyleSheet(s.adjLabelstyle) - s.adjLabel:echo("") - end) - - self:newLockStyle("full", function (s) - s.Inside:resize("100%","100%") - s.Inside:move(0,0) - s.adjLabel:setStyleSheet(string.gsub(s.adjLabelstyle, "(border.-)%d(.-;)","%10%2")) - s.adjLabel:echo("") - end) - - self:newLockStyle("light", function (s) - s:setTitle() - s.Inside:resize("-"..s.padding,"-"..s.padding) - s.Inside:move(s.padding, s.padding*2) - s.adjLabel:setStyleSheet(s.adjLabelstyle) - end) -end - ---- creates a new Lockstyle --- @param name Name of the menu item/lockstyle --- @param func function of the new lockstyle -function Adjustable.Container:newLockStyle(name, func) - if self.lockStyles[name] then - return - end - self.lockStyles[#self.lockStyles + 1] = {name, func} - self.lockStyles[name] = self.lockStyles[#self.lockStyles] - if self.lockStylesLabel then - createMenus(self, "lockStylesLabel", name, "Adjustable.Container.lockContainer") - end -end - ---- creates a new custom menu item --- @param name Name of the new menu item --- @param func function of the new custom menu item -function Adjustable.Container:newCustomItem(name, func) - self.customItems = self.customItems or {} - if self.customItems[name] then - return - end - self.customItems[#self.customItems + 1] = {name, func} - self.customItems[name] = self.customItems[#self.customItems] - createMenus(self, "customItemsLabel", name, "Adjustable.Container.customMenu") -end ---- enablesAutoSave normally only used internally --- only useful if autoSave was set to false before -function Adjustable.Container:enableAutoSave() - self.autoSave = true - self.autoSaveHandler = self.autoSaveHandler or registerAnonymousEventHandler("sysExitEvent", function() self:save() end) -end - ---- disableAutoSave function to disable a before enabled autoSave -function Adjustable.Container:disableAutoSave() - self.autoSave = false - killAnonymousEventHandler(self.autoSaveHandler) -end - ---- constructor for the Adjustable Container ----@param cons besides standard Geyser.Container parameters there are also: ----@param container ---@param[opt="getMudletHomeDir().."/AdjustableContainer/"" ] cons.defaultDir default dir where settings are loaded/saved to/from ---@param[opt="102" ] cons.ParentMenuWidth menu width of the main right click menu ---@param[opt="82"] cons.ChildMenuWidth menu width of the children in the right click menu (for attached, lockstyles and custom items) ---@param[opt="22"] cons.MenuHeight height of a single menu item ---@param[opt="8"] cons.MenuFontSize font size of the menu items ---@param[opt="15"] cons.buttonsize size of the minimize and close buttons ---@param[opt="8"] cons.buttonFontSize font size of the minimize and close buttons ---@param[opt="10"] cons.padding how far is the inside element placed from the corner (depends also on the lockstyle setting) ---@param[opt="5"] cons.attachedMargin margin for the MainWindow border if an adjustable container is attached ---@param cons.adjLabelstyle style of the main Label where all elements are in ---@param cons.menustyle menu items style ---@param cons.buttonstyle close and minimize buttons style ---@param[opt=false] cons.minimized minimized at creation? ---@param[opt=false] cons.locked locked at creation? ---@param[opt=false] cons.attached attached to a border at creation? possible borders are ("top", "bottom", "left", "right") ---@param cons.lockLabel.txt text of the "lock" menu item ---@param cons.minLabel.txt text of the "min/restore" menu item ---@param cons.saveLabel.txt text of the "save" menu item ---@param cons.loadLabel.txt text of the "load" menu item ---@param cons.attLabel.txt text of the "attached menu" item ---@param cons.lockStylesLabel.txt text of the "lockstyle menu" item ---@param cons.customItemsLabel.txt text of the "custom menu" item ---@param[opt="green"] cons.titleTxtColor color of the title text ---@param cons.titleText title text ---@param cons.titleFormat a format list to use.  'c' - center, 'l' - left, 'r' - right, 'b' - bold, 'i' - italics, 'u' - underline, 's' - strikethrough, '##' - font size. ---@param[opt="standard"] cons.lockStyle choose lockstyle at creation.  possible integrated lockstyle are: "standard", "border", "light" and "full" ---@param[opt=false] cons.noLimit there is a minimum size limit if this constraint is set to false. ---@param[opt=true] cons.raiseOnClick raise your container if you click on it with your left mouse button ---@param[opt=true] cons.autoSave saves your container settings on exit (sysExitEvent).  If set to false it won't autoSave ---@param[opt=true] cons.autoLoad loads the container settings (if there are some to load) at creation of the container.  If set to false it won't load the settings at creation - -function Adjustable.Container:new(cons,container) - Adjustable.Container.Locale = Adjustable.Container.Locale or loadTranslations("AdjustableContainer") - cons = cons or {} - cons.type = cons.type or "adjustablecontainer" - local me = self.parent:new(cons, container) +function Adjustable.Container:new(cons, container) + local me = Geyser.Container.new(self, cons, container) setmetatable(me, self) - self.__index = self - me.defaultDir = me.defaultDir or getMudletHomeDir().."/AdjustableContainer/" - me.ParentMenuWidth = me.ParentMenuWidth or "102" - me.ChildMenuWidth = me.ChildMenuWidth or "82" - me.MenuHeight = me.MenuHeight or "22" - me.MenuFontSize = me.MenuFontSize or "8" - me.buttonsize = me.buttonsize or "15" - me.buttonFontSize = me.buttonFontSize or "8" - me.padding = me.padding or 10 - me.attachedMargin = me.attachedMargin or 5 - - me.adjLabelstyle = me.adjLabelstyle or [[ - background-color: rgba(0,0,0,100%); - border: 2px groove white;]] - me.menuStyleMode = "light" - me.buttonstyle= me.buttonstyle or [[ - QLabel{ border-color: rgba(255,255,255,100%); background-color: rgba(0,0,0,100%); } - QLabel::hover{ background-color: rgba(160,160,160,50%); } - ]] - me:createContainers() - me.att = me.att or {} me:createLabels() - me:createRightClickMenu() - - me:globalLockStyles() - me.minimized = me.minimized or false - me.locked = me.locked or false - - me.adjLabelstyle = me.adjLabelstyle..[[ qproperty-alignment: 'AlignLeft | AlignTop';]] - me.lockLabel.txt = me.lockLabel.txt or [[🔒]] ..  self.Locale.lock.message - me.minLabel.txt = me.minLabel.txt or [[🗕]] ..self.Locale.min_restore.message - me.saveLabel.txt = me.saveLabel.txt or [[💾]]..  self.Locale.save.message - me.loadLabel.txt = me.loadLabel.txt or [[📁]]..  self.Locale.load.message - me.attLabel.txt = me.attLabel.txt or [[]]..self.Locale.attach.message - me.lockStylesLabel.txt = me.lockStylesLabel.txt or [[🖌]]..self.Locale.lockstyle.message - me.customItemsLabel.txt = me.customItemsLabel.txt or [[🖇]]..self.Locale.custom.message - - me.adjLabel:setStyleSheet(me.adjLabelstyle) - me.exitLabel:setStyleSheet(me.buttonstyle) - me.minimizeLabel:setStyleSheet(me.buttonstyle) - me:echoRightClickMenu() - - me.adjLabel:setClickCallback("Adjustable.Container.onClick",me, me.adjLabel) - me.adjLabel:setReleaseCallback("Adjustable.Container.onRelease",me, me.adjLabel) - me.adjLabel:setMoveCallback("Adjustable.Container.onMove",me, me.adjLabel) - me.minLabel:setClickCallback("Adjustable.Container.onClickMin", me) - me.saveLabel:setClickCallback("Adjustable.Container.onClickSave", me) - me.lockLabel:setClickCallback("Adjustable.Container.onClickL", me) - me.loadLabel:setClickCallback("Adjustable.Container.onClickLoad", me) - me.origh = me.height - me.exitLabel:setClickCallback("Adjustable.Container.hideObj", me) - me.minimizeLabel:setClickCallback("Adjustable.Container.onClickMin", me) - me.attLabel:setOnEnter("Adjustable.Container.onEnterAtt", me) - me.goInside = true - me.titleTxtColor = me.titleTxtColor or "grey" - me.titleText = me.titleText or me.name.." - Adjustable Container" me:setTitle() - me.lockStyle = me.lockStyle or "standard" - me.noLimit = me.noLimit or false - if not(me.raiseOnClick == false) then - me.raiseOnClick = true - end - - if not Adjustable.Container.all[me.name] then - Adjustable.Container.all_windows[#Adjustable.Container.all_windows + 1] = me.name - else - --prevent showing the container on recreation if hidden is true - if Adjustable.Container.all[me.name].hidden then - me:hide() - end - if Adjustable.Container.all[me.name].auto_hidden then - me:hide(true) - end - -- detach if setting at creation changed - Adjustable.Container.all[me.name]:detach() - end - - if me.minimized then - me:minimize() - end - - if me.locked then - me:lockContainer() - end - - if me.attached then - local attached = me.attached - me.attached = nil - me:attachToBorder(attached) - end - - -- hide/show on creation - if cons.hidden == true then - me:hide() - elseif cons.hidden == false then - me:show() - end - - -- Loads on creation (by Name) if autoLoad is not false - if not(me.autoLoad == false) then - me.autoLoad = true - me:load() - end - - -- Saves on Exit if autoSave is not false - if not(me.autoSave == false) then - me.autoSave = true - me:enableAutoSave() - end - - Adjustable.Container.all[me.name] = me - me:adjustBorder() return me end --- Adjustable Container already uses add2 as it is essential for its functioning (especially for the autoLoad function) --- added this wrapper for consistency -Adjustable.Container.new2 = Adjustable.Container.new +function Adjustable.Container:createLabels() + self.exitLabel = Geyser.Label:new({ + x = -(self.buttonsize * 1.5), y = 2, + width = self.buttonsize, height = self.buttonsize, + name = self.name.."ExitLabel" + }, self.adjLabel) + self.exitLabel:echo("
X") + self.exitLabel:setClickCallback(function() self:hide() end) ---- Overridden constructor to use the old add --- if someone really wants to use the old add for Adjustable Container --- use this function (not recommended) --- or just create elements inside the Adjustable Container with the cons useAdd2 = false -function Adjustable.Container:oldnew(cons, container) - cons = cons or {} - cons.useAdd2 = false - local me = self:new(cons, container) - return me + self.minimizeLabel = Geyser.Label:new({ + x = -(self.buttonsize * 2.8), y = 2, + width = self.buttonsize, height = self.buttonsize, + name = self.name.."MinLabel" + }, self.adjLabel) + self.minimizeLabel:echo("
_") + self.minimizeLabel:setClickCallback(function() + if self.minimized then self:restore() else self:minimize() end + end) + + -- Connect mouse events to the main label + self.adjLabel:setClickCallback(function(l, e) self:onClick(self.adjLabel, e) end) + self.adjLabel:setReleaseCallback(function(l, e) self:onRelease(self.adjLabel, e) end) + self.adjLabel:setMoveCallback(function(l, e) self:onMove(self.adjLabel, e) end) end

When I started mudlet after the patch was applied prior to the build, I saw an improvement in the mudlet turorial too.  I was welcomed with something that was usable.  The image at the top of this blog post is how it looked for me on the mud I am playing.  It took a little adjustment within the UI itself to have it display the way it looks, but it functions.  The UI capability is available for any mud, their UI scripts will be downloaded automatically, possibly after indicating something like "mudlet_ui enable" on the server.

I will be creating all the files needed for a mudlet port that follows release versions.  Mudlet is nearing a new release by the end of May 2026, so this could be a perfect time for an interested party to adopt mudlet and add it to the ports tree.  My next post will provide all of my efforts to get an official style port for mudlet that someone else could maintain as their own, with my blessing.

Sunday, May 10, 2026

Mudlet for FreeBSD ports tree

I have various ways to discover new software that I might try to port (unofficially) to FreeBSD.  Not long ago I was trying to use the linuxulator to run appimages.  One of the things I wanted to work was mudlet, it was among pleny of other games on an appimage index site.  I was unable to succeed, it may have run like a number of others but failed for graphics reasons.  I believe a more direct use via ported software may work.

First I add a new directory into my recently organized GitRepos/PortsTree directory at games/mudlet-dev since I intend as usual to follow upstream commits rather than releases.  I may as well add the SUBDIR += mudlet-dev line to the Makefile in the games directory.  I copy the Makefile from luanti-dev into the mudlet-dev directory because it has a lot of features I might use at another time and also reminder comments on section order.

The essential changes for the Makefile are the PORTNAME, GH_ACCOUNT, GH_PROJECT, and GH_TAGNAME.  Everything else can be ignored or commented out.  The mudlet wiki provides compilation instruction for Linux and even FreeBSD.  This detail should make constructing the Makefile a simpler task than usual.  How refreshing that developers have obviously tested and tried their software on FreeBSD.  An official port would be easy if someone wanted to make one, start from scratch or adjust my Makefile for release version tracking.

Portlint truly needs to comprehend, interpret, and ignore comments in a Makefile.  All of the commented-out parts of the former luanti Makefile were handled fine by the build process, but portlint complained about commented-out port options.  Mudlet has an unusual dependency which seems to be unique, none of our ports seem to require the modules obtained by luarocks.  I had to add a series of commands in special post-extract directive.  This worked perfectly for all but one, and then even avoiding lua-yajl the build continues to a point where it fails on the first git submodule.

We can look at the .gitmodules file to see what it contains, below.

[submodule "3rdparty/edbee-lib"]
	path = 3rdparty/edbee-lib
	url = https://github.com/Mudlet/edbee-lib.git
[submodule "3rdparty/lua_code_formatter"]
	path = 3rdparty/lcf
	url = https://github.com/martin-eden/lua_code_formatter.git
[submodule "3rdparty/qt-tags-widget"]
	path = 3rdparty/qt-tags-widget
	url = https://github.com/julian-go/qt-tags-widget.git
[submodule "3rdparty/qtkeychain"]
	path = 3rdparty/qtkeychain
	url = https://github.com/frankosterfeld/qtkeychain.git
[submodule "3rdparty/sentry-native"]
	path = 3rdparty/sentry-native
	url = https://github.com/getsentry/sentry-native.git

What I found in the porter's handbook may seem strange but it works for figuring out submodules.  It told me to clone the git repo and then use a git command on it.  What I had to do specifically for Mudlet was
git clone --recurse-submodules --branch=development https://github.com/Mudlet/Mudlet.git

root@ichigo:/home/tigersharke/GitRepos # git clone --recurse-submodules --branch=development https://github.com/Mudlet/Mudlet.git
Cloning into 'Mudlet'...
remote: Enumerating objects: 54063, done.
remote: Counting objects: 100% (241/241), done.
remote: Compressing objects: 100% (149/149), done.
remote: Total 54063 (delta 153), reused 95 (delta 92), pack-reused 53822 (from 3)
Receiving objects: 100% (54063/54063), 203.40 MiB | 58.15 MiB/s, done.
Resolving deltas: 100% (40986/40986), done.
Submodule '3rdparty/edbee-lib' (https://github.com/Mudlet/edbee-lib.git) registered for path '3rdparty/edbee-lib'
Submodule '3rdparty/lua_code_formatter' (https://github.com/martin-eden/lua_code_formatter.git) registered for path '3rdparty/lcf'
Submodule '3rdparty/qt-tags-widget' (https://github.com/julian-go/qt-tags-widget.git) registered for path '3rdparty/qt-tags-widget'
Submodule '3rdparty/qtkeychain' (https://github.com/frankosterfeld/qtkeychain.git) registered for path '3rdparty/qtkeychain'
Submodule '3rdparty/sentry-native' (https://github.com/getsentry/sentry-native.git) registered for path '3rdparty/sentry-native'
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/edbee-lib'...
remote: Enumerating objects: 4859, done.         
remote: Counting objects: 100% (301/301), done.         
remote: Compressing objects: 100% (157/157), done.         
remote: Total 4859 (delta 184), reused 170 (delta 140), pack-reused 4558 (from 2)        
Receiving objects: 100% (4859/4859), 3.21 MiB | 3.24 MiB/s, done.
Resolving deltas: 100% (3461/3461), done.
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/lcf'...
remote: Enumerating objects: 1456, done.         
remote: Counting objects: 100% (14/14), done.         
remote: Compressing objects: 100% (10/10), done.         
remote: Total 1456 (delta 5), reused 11 (delta 3), pack-reused 1442 (from 1)        
Receiving objects: 100% (1456/1456), 298.26 KiB | 2.21 MiB/s, done.
Resolving deltas: 100% (549/549), done.
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/qt-tags-widget'...
remote: Enumerating objects: 63, done.         
remote: Counting objects: 100% (63/63), done.         
remote: Compressing objects: 100% (32/32), done.         
remote: Total 63 (delta 22), reused 54 (delta 17), pack-reused 0 (from 0)        
Receiving objects: 100% (63/63), 90.43 KiB | 2.38 MiB/s, done.
Resolving deltas: 100% (22/22), done.
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/qtkeychain'...
remote: Enumerating objects: 1660, done.         
remote: Counting objects: 100% (593/593), done.         
remote: Compressing objects: 100% (196/196), done.         
remote: Total 1660 (delta 483), reused 410 (delta 392), pack-reused 1067 (from 2)        
Receiving objects: 100% (1660/1660), 429.77 KiB | 2.96 MiB/s, done.
Resolving deltas: 100% (1031/1031), done.
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/sentry-native'...
remote: Enumerating objects: 18913, done.         
remote: Counting objects: 100% (1104/1104), done.         
remote: Compressing objects: 100% (347/347), done.         
remote: Total 18913 (delta 962), reused 767 (delta 757), pack-reused 17809 (from 4)        
Receiving objects: 100% (18913/18913), 9.50 MiB | 15.03 MiB/s, done.
Resolving deltas: 100% (13339/13339), done.
Submodule path '3rdparty/edbee-lib': checked out 'a3ae51bbb82158366b3d5c4030a54981db688892'
Submodule path '3rdparty/lcf': checked out '4aa25029eae867840e6c06c7b075f4b690dd2ec2'
Submodule path '3rdparty/qt-tags-widget': checked out '26f177cbcebe66fdc3e8daed4d0984a7f60f3431'
Submodule path '3rdparty/qtkeychain': checked out 'e3b2e83f01cccadf9257c3143ae6a066b7d02149'
Submodule path '3rdparty/sentry-native': checked out 'c0e5f0705da3853ff548c7ece77d639a20e1d8f5'
Submodule 'external/benchmark' (https://github.com/google/benchmark.git) registered for path '3rdparty/sentry-native/external/benchmark'
Submodule 'external/breakpad' (https://github.com/getsentry/breakpad.git) registered for path '3rdparty/sentry-native/external/breakpad'
Submodule 'external/crashpad' (https://github.com/getsentry/crashpad.git) registered for path '3rdparty/sentry-native/external/crashpad'
Submodule 'external/libunwindstack-ndk' (https://github.com/getsentry/libunwindstack-ndk) registered for path '3rdparty/sentry-native/external/libunwindstack-ndk'
Submodule 'external/third_party/lss' (https://github.com/getsentry/chromium-linux-syscall-support) registered for path '3rdparty/sentry-native/external/third_party/lss'
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/sentry-native/external/benchmark'...
remote: Enumerating objects: 10601, done.         
remote: Counting objects: 100% (77/77), done.         
remote: Compressing objects: 100% (45/45), done.         
remote: Total 10601 (delta 62), reused 32 (delta 32), pack-reused 10524 (from 4)        
Receiving objects: 100% (10601/10601), 3.39 MiB | 8.99 MiB/s, done.
Resolving deltas: 100% (7229/7229), done.
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/sentry-native/external/breakpad'...
remote: Enumerating objects: 23942, done.         
remote: Counting objects: 100% (1702/1702), done.         
remote: Compressing objects: 100% (786/786), done.         
remote: Total 23942 (delta 1158), reused 917 (delta 916), pack-reused 22240 (from 4)        
Receiving objects: 100% (23942/23942), 44.66 MiB | 24.51 MiB/s, done.
Resolving deltas: 100% (18247/18247), done.
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/sentry-native/external/crashpad'...
remote: Enumerating objects: 27408, done.         
remote: Counting objects: 100% (16942/16942), done.         
remote: Compressing objects: 100% (2051/2051), done.         
remote: Total 27408 (delta 15729), reused 14899 (delta 14890), pack-reused 10466 (from 3)        
Receiving objects: 100% (27408/27408), 10.16 MiB | 17.99 MiB/s, done.
Resolving deltas: 100% (21336/21336), done.
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/sentry-native/external/libunwindstack-ndk'...
remote: Enumerating objects: 1714, done.         
remote: Counting objects: 100% (518/518), done.         
remote: Compressing objects: 100% (187/187), done.         
remote: Total 1714 (delta 385), reused 364 (delta 318), pack-reused 1196 (from 1)        
Receiving objects: 100% (1714/1714), 829.10 KiB | 4.15 MiB/s, done.
Resolving deltas: 100% (1190/1190), done.
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/sentry-native/external/third_party/lss'...
remote: Enumerating objects: 311, done.         
remote: Counting objects: 100% (311/311), done.         
remote: Compressing objects: 100% (150/150), done.         
remote: Total 311 (delta 159), reused 311 (delta 159), pack-reused 0 (from 0)        
Receiving objects: 100% (311/311), 549.84 KiB | 3.05 MiB/s, done.
Resolving deltas: 100% (159/159), done.
Submodule path '3rdparty/sentry-native/external/benchmark': checked out '48f5cc21bac647a8a64e9787cb84f349e334b7ac'
Submodule path '3rdparty/sentry-native/external/breakpad': checked out '25b6b727af49fa383161e7dba4a82ab0661b69b8'
Submodule path '3rdparty/sentry-native/external/crashpad': checked out '17b7aca1634f1a91018f1bba13f7941a2892e864'
Submodule 'third_party/lss/lss' (https://github.com/getsentry/chromium-linux-syscall-support) registered for path '3rdparty/sentry-native/external/crashpad/third_party/lss/lss'
Submodule 'third_party/mini_chromium/mini_chromium' (https://github.com/getsentry/mini_chromium.git) registered for path '3rdparty/sentry-native/external/crashpad/third_party/mini_chromium/mini_chromium'
Submodule 'third_party/zlib/zlib' (https://github.com/getsentry/chromium-zlib) registered for path '3rdparty/sentry-native/external/crashpad/third_party/zlib/zlib'
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/sentry-native/external/crashpad/third_party/lss/lss'...
remote: Enumerating objects: 311, done.         
remote: Counting objects: 100% (311/311), done.         
remote: Compressing objects: 100% (150/150), done.         
remote: Total 311 (delta 159), reused 311 (delta 159), pack-reused 0 (from 0)        
Receiving objects: 100% (311/311), 549.84 KiB | 3.23 MiB/s, done.
Resolving deltas: 100% (159/159), done.
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/sentry-native/external/crashpad/third_party/mini_chromium/mini_chromium'...
remote: Enumerating objects: 2810, done.         
remote: Counting objects: 100% (995/995), done.         
remote: Compressing objects: 100% (206/206), done.         
remote: Total 2810 (delta 868), reused 790 (delta 789), pack-reused 1815 (from 2)        
Receiving objects: 100% (2810/2810), 867.24 KiB | 2.83 MiB/s, done.
Resolving deltas: 100% (1900/1900), done.
Cloning into '/home/tigersharke/GitRepos/Mudlet/3rdparty/sentry-native/external/crashpad/third_party/zlib/zlib'...
remote: Enumerating objects: 2999, done.         
remote: Counting objects: 100% (2999/2999), done.         
remote: Compressing objects: 100% (1070/1070), done.         
remote: Total 2999 (delta 1885), reused 2999 (delta 1885), pack-reused 0 (from 0)        
Receiving objects: 100% (2999/2999), 1.68 MiB | 6.48 MiB/s, done.
Resolving deltas: 100% (1885/1885), done.
Submodule path '3rdparty/sentry-native/external/crashpad/third_party/lss/lss': checked out '9719c1e1e676814c456b55f5f070eabad6709d31'
Submodule path '3rdparty/sentry-native/external/crashpad/third_party/mini_chromium/mini_chromium': checked out '64339ac9468a8c3af236ca9186b42a33354455b9'
Submodule path '3rdparty/sentry-native/external/crashpad/third_party/zlib/zlib': checked out 'fef58692c1d7bec94c4ed3d030a45a1832a9615d'
Submodule path '3rdparty/sentry-native/external/libunwindstack-ndk': checked out '284202fb1e42dbeba6598e26ced2e1ec404eecd1'
Submodule path '3rdparty/sentry-native/external/third_party/lss': checked out 'ed31caa60f20a4f6569883b2d752ef7522de51e0'
root@ichigo:/home/tigersharke/GitRepos #

The next step is to cd into the Mudlet directory and issue git submodule status which gave me the requisite hashes for all of the submodules.

root@ichigo:/home/tigersharke/GitRepos/Mudlet # git submodule status
 a3ae51bbb82158366b3d5c4030a54981db688892 3rdparty/edbee-lib (v0.1.0-386-ga3ae51b)
 4aa25029eae867840e6c06c7b075f4b690dd2ec2 3rdparty/lcf (5.1-2-4-g4aa2502)
 26f177cbcebe66fdc3e8daed4d0984a7f60f3431 3rdparty/qt-tags-widget (heads/main)
 e3b2e83f01cccadf9257c3143ae6a066b7d02149 3rdparty/qtkeychain (0.15.0-44-ge3b2e83)
 c0e5f0705da3853ff548c7ece77d639a20e1d8f5 3rdparty/sentry-native (0.4.14-666-gc0e5f070)

When you've gone round and round with attempting to get a port to build with the Makefile you've constructed for it, and it finally succeeds, it becomes difficult to know exactly what you did to cause it.  This gets to be especially true when the issue is a dependency that you were sure was accurate any number of previous tries, but this last time it is found and the port builds.  Undoubtedly the change was in the Makefile, and after this sudden and unexpected success, I must capture the current state of the Makefile into my git repo for this nascent port.  I've made more improvements to the Makefile as it is now shown below.  The pkg-plist and other files are created and on my github repo site for Mudlet.

### PORTNAME block ##--------------------------------------------------------------------------------------
PORTNAME=		Mudlet
DISTVERSION=	g20260509
CATEGORIES=		games
MASTER_SITES=	GH
PKGNAMESUFFIX=	-dev
DIST_SUBDIR=	${PORTNAME}${PKGNAMESUFFIX}

# Maintainer block ##--------------------------------------------------------------------------------------
MAINTAINER=		nope@nothere
COMMENT=		Cross-platform, open source, super fast MUD client with lua scripting
WWW=			https://mudlet.org/

### License block ##---------------------------------------------------------------------------------------
LICENSE=		GPLv2+
LICENSE_FILE=	${WRKSRC}/COPYING

# dependencies ##------------------------------------------------------------------------------------------
LIB_DEPENDS=	libassimp.so:multimedia/assimp \
				libqt6keychain.so:security/qtkeychain@qt6 \
				libpugixml.so:textproc/pugixml \
				libhunspell-1.7.so:textproc/hunspell \
				libpcre2-8.so:devel/pcre2 \
				libzip.so:archivers/libzip \
				libsysinfo.so:devel/libsysinfo \
				libonig.so:devel/oniguruma \
				libzstd.so:archivers/zstd \
				libcurl.so:ftp/curl \
				libboost_thread.so:devel/boost-libs \
				liblua-5.1.so:lang/lua51
#				libyajl.so:devel/yajl \

BUILD_DEPENDS=	lua54-luarocks>0:devel/lua-luarocks@lua54

### uses block ##------------------------------------------------------------------------------------------
USES=			lua:51 cmake:noninja gmake sqlite qt:6 desktop-file-utils gl

GH_ACCOUNT= Mudlet
GH_TAGNAME= 4e32ebc58d0abc58418f03145ef54b3b7b7093f8
USE_GITHUB= nodefaults
GH_TUPLE= \
			Mudlet:edbee-lib:a3ae51bbb82158366b3d5c4030a54981db688892:fakedir1/3rdparty/edbee-lib \
			martin-eden:lua_code_formatter:4aa25029eae867840e6c06c7b075f4b690dd2ec2:fakedir2/3rdparty/lcf \
			julian-go:qt-tags-widget:26f177cbcebe66fdc3e8daed4d0984a7f60f3431:fakedir3/3rdparty/qt-tags-widget \
			getsentry:sentry-native:c0e5f0705da3853ff548c7ece77d639a20e1d8f5:fakedir5/3rdparty/sentry-native

USE_QT=			base 5compat multimedia tools
USE_GL=			gl opengl glu

# USES=cmake related variables ##--------------------------------------------------------------------------
#
### Make block ##------------------------------------------------------------------------------------------
#
### conflicts ##-------------------------------------------------------------------------------------------
CONFLICTS=		Mudlet mudlet
### wrksrc block ##----------------------------------------------------------------------------------------
#
### packaging list block ##--------------------------------------------------------------------------------
#
### options definitions ##---------------------------------------------------------------------------------
#
### options descriptions ##--------------------------------------------------------------------------------
#
### options helpers ##-------------------------------------------------------------------------------------
#
.include 

post-stage:
	${MKDIR} ${STAGEDIR}${LOCALBASE}/share/lua/5.1/
	cd ${STAGEDIR}${LOCALBASE} && ${LOCALBASE}/bin/luarocks54 --tree= --lua-version 5.1 install luautf8
	cd ${STAGEDIR}${LOCALBASE} && ${LOCALBASE}/bin/luarocks54 --tree= --lua-version 5.1 install luafilesystem
	cd ${STAGEDIR}${LOCALBASE} && ${LOCALBASE}/bin/luarocks54 --tree= --lua-version 5.1 install lua-zip
	cd ${STAGEDIR}${LOCALBASE} && ${LOCALBASE}/bin/luarocks54 --tree= --lua-version 5.1 install luasql-sqlite3
	cd ${STAGEDIR}${LOCALBASE} && ${LOCALBASE}/bin/luarocks54 --tree= --lua-version 5.1 install lrexlib-pcre2
	cd ${STAGEDIR}${LOCALBASE} && ${LOCALBASE}/bin/luarocks54 --tree= --lua-version 5.1 install lpeg
	cd ${STAGEDIR}${LOCALBASE} && ${LOCALBASE}/bin/luarocks54 --tree= --lua-version 5.1 install lua-yajl
	cp -R /usr/local/share/lua/lib/lua/5.1/* /usr/local/lib/lua/5.1/

# The above is definitely weird but I believe everything gets placed where it must for mudlet features to work.

#----------------------------------------------------------------------

.include 

What took me the longest to perfect were the tuples and getting them to be placed in the correct locations.  What baffles me the most about this, is how it was successful, since I used a series of numbered 'fakedir' directories which are entirely ignored.  Should those "fakedir" entries actually be some kind of other place-holder word?  What I did works for now, when it breaks I will update it to the proper method.  The handbook is less than clear about handling a git submodule arrangement like this.  A REAL example would be much more helpful than foo and bar and all that confusing stuff because I could go to the real port to see the whole process, step through the directories created and truly understand it.  One other thing that doesn't seem to be mentioned in the handbook anywhere that I could locate, is how to have a dependency upon a specific flavored port.  I had to depend upon one of the two possible flavors for a dependency libqt6keychain.so:security/qtkeychain@qt6 which I happened to partially guess after trying other things that generated errors.

I've lightly tested Mudlet to see what it is like, it works fine but I didn't try to play any MUD games yet.  The work I have done to take a known FreeBSD build mentioned on the Mudlet wiki to a Makefile usable in our ports tree but that mine tracks upstream commits instead of release versions.  You are very welcome, whomever is interested, to take all my efforts, lightly revise them for release version tracking, possibly correct any of my style or technique mistakes, and maybe create a better method for the luarocks portion.  There is an easy way to install Mudlet now, the rest is up to a volunteer with my blessing.  My success would never have been possible without the work of erikarn (adrian@freebsd.org).

Friday, May 8, 2026

Trying portscout for unofficial ports

My collection of unofficial ports which track upstream commits is getting a bit large.  I had been keeping a tab in firefox to each git repo commits/master or commits/main page open.  I also keep a tab for each of my own repos because I may not know when I made my own update the last time.  I have known that portscout exists and discovered that it is in our ports tree at ports-mgmt/portscout.

I am going to try to setup portscout for my collection of unofficial ports.  Not long ago I organized slightly better by placing them all in one directory, GitRepos.  I have so far avoided organizing them as my own sparse ports tree.  Placing all the various unofficial ports into GitRepos also lets me add that directory as a shortcut in bluefish so I can include files when useful.

Installing the port is simple, I chose to build it but I could have installed with pkg.

INITIAL SET-UP
   Initialise Database
     The recommended database backend is PostgreSQL.

     Option One: PostgreSQL

     Create database:

         # createuser -U pgsql -P portscout
         # createdb -U pgsql portscout

     Execute the included pgsql_init.sql script via "psql":

         # psql portscout portscout < sql/pgsql_init.sql

     This will create the database tables for you.

According to the PORTSCOUT(1) manpage I am supposed to create a user, then create the database.

root@ichigo:/home/tigersharke/GitRepos # createuser -U pgsql -P portscout
Enter password for new role: 
Enter it again: 
createuser: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed: No such file or directory
        Is the server running locally and accepting connections on that socket?
root@ichigo:/home/tigersharke/GitRepos #

What seems to be necessary is databases/postgresql18-server because the first step described in the portscout manpage failed, or perhaps something failed to be installed.  I installed the server and looked at the output messages to see what it may tell me about getting things going.

To use PostgreSQL, enable it in rc.conf using

  sysrc postgresql_enable=yes

To initialize the database, run

  service postgresql initdb

You can then start PostgreSQL by running:

  service postgresql start

This is what I did, using the instructions described in the order given above.

root@ichigo:/home/tigersharke/GitRepos # sysrc postgresql_enable=yes
postgresql_enable:  -> yes
root@ichigo:/home/tigersharke/GitRepos # service postgresql initdb
initdb postgresql
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with this locale configuration:
  locale provider:   libc
  LC_COLLATE:  C
  LC_CTYPE:    C.UTF-8
  LC_MESSAGES: C.UTF-8
  LC_MONETARY: C.UTF-8
  LC_NUMERIC:  C.UTF-8
  LC_TIME:     C.UTF-8
The default text search configuration will be set to "english".

Data page checksums are enabled.

creating directory /var/db/postgres/data18 ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default "max_connections" ... 100
selecting default "shared_buffers" ... 128MB
selecting default time zone ... America/Chicago
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    /usr/local/bin/pg_ctl -D /var/db/postgres/data18 -l logfile start

root@ichigo:/home/tigersharke/GitRepos # service postgresql start
start postgresql
root@ichigo:/home/tigersharke/GitRepos #

Now when I try the first part of the portscout setup, I get a different error so it appears that I need to investigate postgres sql server a bit.

root@ichigo:/home/tigersharke/GitRepos # createuser -U pgsql -P portscout
Enter password for new role: 
Enter it again: 
createuser: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed: FATAL:  role "pgsql" does not exist
root@ichigo:/home/tigersharke/GitRepos #

According to a FreeBSD forum post I believe I need to change 'pgsql' to 'postgres' for that command, so I will try this and see if it succeeds.  It turns out to be the piece I missed and which may need to be changed in the portscout manpage.

root@ichigo:/home/tigersharke/GitRepos # createuser -U postgres -P portscout
Enter password for new role: 
Enter it again: 
root@ichigo:/home/tigersharke/GitRepos # createdb -U postgres portscout
root@ichigo:/home/tigersharke/GitRepos # psql portscout portscout < sql/pgsql_init.sql
sql/pgsql_init.sql: No such file or directory.
root@ichigo:/home/tigersharke/GitRepos # postgres portscout portscout < sql/pgsql_init.sql
sql/pgsql_init.sql: No such file or directory.
root@ichigo:/home/tigersharke/GitRepos # locate sql/pgsql_init.sql
/usr/local/share/portscout/sql/pgsql_init.sql
root@ichigo:/home/tigersharke/GitRepos # postgres portscout portscout < /usr/local/share/portscout/sql/pgsql_init.sql
"root" execution of the PostgreSQL server is not permitted.
The server must be started under an unprivileged user ID to prevent
possible system security compromise.  See the documentation for
more information on how to properly start the server.
root@ichigo:/home/tigersharke/GitRepos #

Easy enough to go to my user and start it there.

tigersharke@ichigo [~] % psql portscout portscout < /usr/local/share/portscout/sql/pgsql_init.sql
ERROR:  permission denied for schema public
LINE 9: CREATE TABLE portdata (
                     ^
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE sitedata (
                     ^
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE moveddata (
                     ^
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE maildata (
                     ^
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE systemdata (
                     ^
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE allocators (
                     ^
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE portscout (
                     ^
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE stats (
                     ^
ERROR:  relation "portscout" does not exist
LINE 2:   INTO portscout (dbver)
               ^
ERROR:  relation "stats" does not exist
LINE 2:   INTO stats (key)
               ^
ERROR:  relation "portdata" does not exist
ERROR:  relation "portdata" does not exist
ERROR:  relation "portdata" does not exist
ERROR:  relation "portdata" does not exist
ERROR:  relation "portdata" does not exist
ERROR:  relation "sitedata" does not exist
ERROR:  relation "moveddata" does not exist
tigersharke@ichigo [~] %

There is obviously a bit more to setup and configure but I believe the majority of portscout is ready to go.  It looks like I need to copy /usr/local/etc/portscout.conf.sample to ~/portscout.conf and then edit to fit my needs.  At github portscout.pod other documentation is available.  As is common, if I do not define a configuration file, it will use an internal default, so I believe that is why I got so many errors above.

I made quite a few changes from the sample to my own custom configuration file, most notably that I will need to create an xml file to control how it will search for updates in GitRepo for my unofficial -dev ports.

#------------------------------------------------------------------------------
# portscout config file
#
# Format:
#   - Comments begin with '#' and extend to the end of the line
#   - Variables are case insensitive, and may use spaces or underscores as word
#     separators (i.e. ports dir == ports_dir)
#   - Variables are separated from their values by a single '='
#   - Paths must have no trailing slash
#   - Use quotes if you need to retain leading/trailing whitespace
#   - You can reuse previously set variables, like: %(name) - these variables
#     must use underscores, not spaces.
#
# $Id: portscout.conf,v 1.16 2011/04/09 17:17:34 samott Exp $
#------------------------------------------------------------------------------

# Space saving variables (only used within this file)

prefix           = /usr/local
tmpdir           = /tmp
wwwdir           = %(prefix)/www/data

#-- Data Provider -------------------------------------------------------------

# The DataSrc module is what portscout uses to compile information
# into its internal database. In other words, it's the layer between
# the repository of software and portscout itself.

# Option One: FreeBSD ports (NetBSD and OpenBSD supported too)

#datasrc          = Portscout::DataSrc::Ports
#datasrc opts     = type:NetBSD

# Option Two: XML file

datasrc          = Portscout::DataSrc::XML
#datasrc opts     = file:%(prefix)/etc/portscout/software.xml
datasrc opts     = file:/home/tigerharke/GitRepos/software.xml

#-- User Privileges -----------------------------------------------------------

# If these are not empty, portscout will switch to this
# user/group as soon as is practical after starting (if it
# is running as root).

user             = portscout
group            = portscout

#-- Directories ---------------------------------------------------------------

ports dir        = /usr/ports          		# Ports root directory

html data dir    = %(wwwdir)/portscout 		# Where to put generated HTML

templates dir    = %(prefix)/share/portscout/templates # Where HTML templates are kept

#-- Limit Processing ----------------------------------------------------------

# The following three variables are comma-separated lists of
# items that portscout should process. If left empty, portscout
# will not limit itself, and will process the whole ports tree.

# Items in the list may contain * and ? wildcard characters.

restrict maintainer =        				# Limit to these maintainers
restrict category   =        				# "     "  "     categories
restrict port       =        				# "     "  "     ports

# Note that if you set restrict_maintainer, the entire ports
# tree needs to be processed to ascertain which ports meet
# the restriction criterion. This can be avoided if portscout
# has access to an INDEX file. If you don't have an INDEX file,
# and aren't impatient, you can switch off the following.
# With no maintainer restriction in place, it has no effect.

indexfile enable    = true   				# Use INDEX if needed

#-- Mailing Settings ----------------------------------------------------------

# These are only required if you plan to send out reminder mails
# It is enabled by default because you will need to add some
# addresses to the database for anything to happen anyway.

# The sender address will have the local hostname attached if it
# is a bare username.

#mail enable                = true
mail enable                = false

mail from                  = portscout 			# Sender address
mail subject               = FreeBSD ports you maintain which are out of date
mail subject unmaintained  = Unmaintained FreeBSD ports which are out of date
mail method                = sendmail  			# Can be 'sendmail' or 'smtp'
#mail host                  = localhost			# SMTP server, if method is 'smtp'

#-- Output Settings -----------------------------------------------------------

# Timezone options. This is just eye-candy for template generation,
# but setting it to anything other than 'GMT' will cause portscout
# to use the local time, rather than GMT.

local timezone   = GMT       				# Use Greenwich Time

# Hide results for ports with no new distfile?

hide unchanged   = false     				# Show ports with no updates.

#-- Other Settings ------------------------------------------------------------

mastersite limit = 4         				# Give up after this many sites

oldfound enable  = true      				# Stop if curr. distfile found

precious data    = false     				# Don't write anything to database
num children     = 15        				# How many worker children to spawn
workqueue size   = 20        				# How many ports per child at a time

# This variable specifies what version comparison algorithm
# to use. Supported values are "internal" and "pkg_version";
# the latter uses 'pkg_version -t', which is pretty straight-
# forward, but makes no attempt at best-guessing backwards
# looking version numbers. The former is a bit more
# sophisticated.

version compare  = internal  				# Version algorithm to use

# It is possible for individual ports to give us information
# such as the "limit version" regex. The following variable
# enables this.

portconfig enable = true     				# Respect port config hints

# If you're using portscout with a something other than the
# FreeBSD ports tree, switch this off to disable rejection of
# non-FreeBSD distfiles (such as 1.3.2-win32.zip).

#freebsdhacks enable = true
freebsdhacks enable = false

# HTTP/FTP options

http timeout     = 120       				# Timeout in seconds

ftp timeout      = 120       				# Timeout in seconds
ftp passive      = true      				# Try to use passive FTP
ftp retries      = 3         				# Give up after this many failures

# The following tell portscout how to deal with sites which have a robots.txt
# file. Possible values:
#   standard - Check for robots.txt but only respect portscout-specific bans.
#   strict   - Respect all bans, including '*' wildcards.
#
# You can disable any robots checks with robots_enable. But think twice
# before doing so: angry system admins are likely to block bots they don't
# like using other methods.
#
# Plenty of sites have blanket robot bans, intended to stop search engine
# crawlers from indexing pages, and thus 'strict' is likely to affect the
# number of results we can gather.

robots enable    = true      				# Check for robots.txt files
robots checking  = strict    				# Strict robots.txt checking

# Database connection details

db user          = portscout 				# Database username
db name          = portscout 				# Database name
db pass          =           				# Password

# These two are only used for db_connstr, below

db host          =           				# Host
db port          =           				# Port

db connstr       = DBI:Pg:dbname=%(db_name)
#db connstr       = DBI:Pg:dbname=%(db_name);host=%(db_host);port=%(db_port)
#db connstr       = DBI:SQLite:dbname=/var/db/portscout.db

# GitHub site handler settings
# GitHub rate limits requests to its API to a very low number for unauthenticated
# requests, and 5000 per hour for authenticated requests.
# GitHub personal access tokens can be requested on github accounts that
# have a verified email address here: https://github.com/settings/tokens
# A public personal access token without any special permissions will do(!)

#github token     =             # GitHub personal access token
github token     = ghQ_nG4JUadwpM2RcAQIMkneVX7C4zN96Z24IkQG

# ex: ts=4 sw=4

I believe the format of the xml file is like below, which I found in https://github.com/freebsd/portscout/blob/master/Portscout/DataSrc/XML.pm and I'm sure exists amongst the portscout install but I don't know where.  It seems like the details in the xml file ought to be obtained from the Makefile or distfile and looks like a lot of excess typing, is there something that would generate this?  Not only that, but items like version would need to be revised each time I update the Makefiles, couldn't those specific details be pulled into an updated xml file so I would only need to edit the Makefile each time?

    <items>
        <item>
            <category>software</category>
            <name>foo</name>
            <desc>Foomatic Professional</desc>
            <version>0.4.3</version>
            <suffix>.tar.gz</suffix>
            <distname>%(name)-%(version)</distname>
            <distfiles>
                <file>%(distname)%(suffix)</file>
            </distfiles>
            <sites>
                <site>http://foo.example.net/releases/</site>
                <site>ftp://mirror.local/pub/foo/</site>
            </sites>
            <options>
                <option name="limit" value="1,even" />
            </options>
        </item>
        <item>
            <category>software</category>
            <name>bar</name>
            <desc>Barware</desc>
            <version>1.8</version>
            <sites>
                <site>http://example.org/software/bar/</site>
            </sites>
        </item>
    </items>

What I will try is to create the xml file as above but only include a small portion of the content, and only for one of the unofficial ports, likely luanti-dev.  After the file exists and I point portscout at it (in the config file I believe) then I will see what it does, will it check the url for the git tag and compare it to what is in the xml file?

I have been running around in circles, periodically smashing my head against a brick wall.  I decided to ask Grok to see if I could get some coherent documentation or at least an understanding of all of it because I have obviously missed something.

**Portscout can be configured to scan a custom/local ports tree (or a subset of one) containing your unofficial ports.** It does not directly scan arbitrary distfile directories for version checks in the way you described (i.e., reading local distfiles first and then probing upstream). Instead, it parses each port's `Makefile` (via `make`) to extract `DISTNAME`/`DISTVERSION`, `MASTER_SITES` (including GitHub/GitLab via `USE_GITHUB`/`USE_GITLAB`), `PORTSCOUT` hints, etc., then checks those upstream sites for newer versions.

### Recommended Setup for Local/Unofficial Ports

1. **Create a ports-like directory structure**  
   Organize your local ports like this (you can mix with a full tree or keep it separate):

   ```
   /path/to/my-local-ports/
   ├── category1/
   │   └── myport1/
   │       ├── Makefile
   │       ├── distinfo
   │       └── (other files)
   ├── category2/
   │   └── myport2/
   └── Makefile  (optional top-level with SUBDIR entries)
   ```

   Each `Makefile` should define the usual variables (`PORTNAME`, `DISTVERSION` or `DISTNAME`, `MASTER_SITES`, `USE_GITHUB`, `USE_GITLAB`, etc.). Portscout relies heavily on these.

2. **Configure portscout** (usually `/usr/local/etc/portscout.conf` or wherever you installed it)  
   Key settings:

   ```
   ports dir = /path/to/my-local-ports
   datasrc   = Portscout::DataSrc::Ports
   # datasrc opts = type:FreeBSD   (default; works for custom trees too)
   ```

   You can also use restrictions to scan only specific ports/categories:

   ```
   restrict category = category1,category2
   restrict port     = myport1,myport2
   restrict maintainer = your@email.com
   ```

   Other useful options:
   - `portconfig enable = true` (respects `PORTSCOUT=` hints in Makefiles).
   - `freebsdhacks enable = true` (or false if your ports aren't FreeBSD-style).
   - GitHub/GitLab support is built-in (including tokens for rate limits).

3. **Initialize / Populate the Database**  
   ```sh
   portscout build          # Full initial build (parses all Makefiles)
   # or
   portscout rebuild        # Incremental (faster for updates)
   ```

4. **Run Checks**  
   ```sh
   portscout check          # Checks upstream for new versions
   portscout showupdates    # Console summary of updates
   portscout generate       # HTML reports
   # portscout mail         # Email notifications (opt-in)
   ```

### Enhancing Individual Local Ports (Makefile Level)

Add a `PORTSCOUT` variable in your local `Makefile`s for better behavior:

```
PORTSCOUT= site:https://github.com/user/repo/releases  # Prefer specific page
# or
PORTSCOUT= limit:^\d+\.\d+  # Regex to filter versions
# PORTSCOUT= skipb:0        # Accept betas if desired
# PORTSCOUT= ignore:1       # Skip this port entirely
```

For GitHub/GitLab releases, standard `USE_GITHUB= yes` + `GH_ACCOUNT` / `GH_PROJECT` / `GH_TAGNAME` works well—portscout knows how to handle them.

### Distfile Handling Notes

- Portscout does **not** primarily read your local `distfiles/` directories to determine the "current" version for comparison. It uses the version from the `Makefile` (`DISTNAME`/`DISTVERSION`).
- It then probes the `MASTER_SITES` (and any `PORTSCOUT= site:...` hints) for newer files matching the expected naming pattern.
- If you want it to consider a local distfile's presence, ensure your `distinfo` and `Makefile` are consistent, and the port builds cleanly with `make fetch`.
- You can set `DISTDIR` in the environment or Makefile if your distfiles live elsewhere, but this is secondary for portscout's version-checking logic.

### Tips and Limitations

- **Performance**: For a small local tree, reduce `num_children` and `workqueue_size` in the config.
- **Incremental scans**: `portscout rebuild` + `check` is efficient if you only update a few ports.
- **Custom make**: If your tree isn't standard FreeBSD, you may need to adjust `Portscout::Make` behavior or test with a small subset.
- **Multiple trees**: You could maintain separate config files/databases for official vs. local ports and run them independently.
- Portscout's Ports backend walks category directories looking for subdirs with Makefiles, so a clean tree structure is key.

For full details, see the portscout source/docs (especially `portscout.pod`, `portscout.conf`, and `docs/portscout-portconfig.txt`). Install via `pkg install portscout` or from ports-mgmt/portscout, then run `perldoc portscout` or check the GitHub repo.

This setup should let you efficiently track upstream updates for your local/unofficial ports using their defined mastersites.
So I adjusted some lines in the portscout.conf but what I also realized is that portscout only looks in /usr/local/etc/ for the portscout.conf.
#------------------------------------------------------------------------------
# portscout config file
#
# Format:
#   - Comments begin with '#' and extend to the end of the line
#   - Variables are case insensitive, and may use spaces or underscores as word
#     separators (i.e. ports dir == ports_dir)
#   - Variables are separated from their values by a single '='
#   - Paths must have no trailing slash
#   - Use quotes if you need to retain leading/trailing whitespace
#   - You can reuse previously set variables, like: %(name) - these variables
#     must use underscores, not spaces.
#
# $Id: portscout.conf,v 1.16 2011/04/09 17:17:34 samott Exp $
#------------------------------------------------------------------------------

# Space saving variables (only used within this file)

prefix           = /usr/local
tmpdir           = /tmp
wwwdir           = %(prefix)/www/data

#-- Data Provider -------------------------------------------------------------

# The DataSrc module is what portscout uses to compile information
# into its internal database. In other words, it's the layer between
# the repository of software and portscout itself.

# Option One: FreeBSD ports (NetBSD and OpenBSD supported too)

datasrc          = Portscout::DataSrc::Ports
#datasrc opts     = type:NetBSD

# Option Two: XML file

#datasrc          = Portscout::DataSrc::XML
#datasrc opts     = file:%(prefix)/etc/portscout/software.xml

#-- User Privileges -----------------------------------------------------------

# If these are not empty, portscout will switch to this
# user/group as soon as is practical after starting (if it
# is running as root).

user             = portscout
group            = portscout

#-- Directories ---------------------------------------------------------------

#ports dir        = /usr/ports          		# Ports root directory
ports dir        = /home/tigersharke/GitRepos/PortsTree          		# Ports root directory

html data dir    = %(wwwdir)/portscout 		# Where to put generated HTML

templates dir    = %(prefix)/share/portscout/templates # Where HTML templates are kept

#-- Limit Processing ----------------------------------------------------------

# The following three variables are comma-separated lists of
# items that portscout should process. If left empty, portscout
# will not limit itself, and will process the whole ports tree.

# Items in the list may contain * and ? wildcard characters.

restrict maintainer =        				# Limit to these maintainers
restrict category   =        				# "     "  "     categories
restrict port       =        				# "     "  "     ports

# Note that if you set restrict_maintainer, the entire ports
# tree needs to be processed to ascertain which ports meet
# the restriction criterion. This can be avoided if portscout
# has access to an INDEX file. If you don't have an INDEX file,
# and aren't impatient, you can switch off the following.
# With no maintainer restriction in place, it has no effect.

#indexfile enable    = true   				# Use INDEX if needed
indexfile enable    = false   				# Use INDEX if needed

#-- Mailing Settings ----------------------------------------------------------

# These are only required if you plan to send out reminder mails
# It is enabled by default because you will need to add some
# addresses to the database for anything to happen anyway.

# The sender address will have the local hostname attached if it
# is a bare username.

#mail enable                = true
mail enable                = false

mail from                  = portscout 			# Sender address
mail subject               = FreeBSD ports you maintain which are out of date
mail subject unmaintained  = Unmaintained FreeBSD ports which are out of date
mail method                = sendmail  			# Can be 'sendmail' or 'smtp'
#mail host                  = localhost			# SMTP server, if method is 'smtp'

#-- Output Settings -----------------------------------------------------------

# Timezone options. This is just eye-candy for template generation,
# but setting it to anything other than 'GMT' will cause portscout
# to use the local time, rather than GMT.

local timezone   = GMT       				# Use Greenwich Time

# Hide results for ports with no new distfile?

hide unchanged   = false     				# Show ports with no updates.

#-- Other Settings ------------------------------------------------------------

mastersite limit = 4         				# Give up after this many sites

oldfound enable  = true      				# Stop if curr. distfile found

precious data    = false     				# Don't write anything to database
num children     = 15        				# How many worker children to spawn
workqueue size   = 20        				# How many ports per child at a time

# This variable specifies what version comparison algorithm
# to use. Supported values are "internal" and "pkg_version";
# the latter uses 'pkg_version -t', which is pretty straight-
# forward, but makes no attempt at best-guessing backwards
# looking version numbers. The former is a bit more
# sophisticated.

version compare  = internal  				# Version algorithm to use

# It is possible for individual ports to give us information
# such as the "limit version" regex. The following variable
# enables this.

portconfig enable = true     				# Respect port config hints

# If you're using portscout with a something other than the
# FreeBSD ports tree, switch this off to disable rejection of
# non-FreeBSD distfiles (such as 1.3.2-win32.zip).

#freebsdhacks enable = true
freebsdhacks enable = false

# HTTP/FTP options

http timeout     = 120       				# Timeout in seconds

ftp timeout      = 120       				# Timeout in seconds
ftp passive      = true      				# Try to use passive FTP
ftp retries      = 3         				# Give up after this many failures

# The following tell portscout how to deal with sites which have a robots.txt
# file. Possible values:
#   standard - Check for robots.txt but only respect portscout-specific bans.
#   strict   - Respect all bans, including '*' wildcards.
#
# You can disable any robots checks with robots_enable. But think twice
# before doing so: angry system admins are likely to block bots they don't
# like using other methods.
#
# Plenty of sites have blanket robot bans, intended to stop search engine
# crawlers from indexing pages, and thus 'strict' is likely to affect the
# number of results we can gather.

robots enable    = true      				# Check for robots.txt files
robots checking  = strict    				# Strict robots.txt checking

# Database connection details

db user          = portscout 				# Database username
db name          = portscout 				# Database name
db pass          = portscout    			# Password

# These two are only used for db_connstr, below

#db host          =           				# Host
#db port          =           				# 
db host			= localhost                   # or 127.0.0.1
db port			= 5432

db connstr       = DBI:Pg:dbname=%(db_name)
#db connstr       = DBI:Pg:dbname=%(db_name);host=%(db_host);port=%(db_port)
#db connstr       = DBI:SQLite:dbname=/var/db/portscout.db

# GitHub site handler settings
# GitHub rate limits requests to its API to a very low number for unauthenticated
# requests, and 5000 per hour for authenticated requests.
# GitHub personal access tokens can be requested on github accounts that
# have a verified email address here: https://github.com/settings/tokens
# A public personal access token without any special permissions will do(!)

#github token     =				# GitHub personal access token
github token     = ghQ_nG4JUadwpM2RcAQIMkneVX7C4zN96Z24IkQG


# ex: ts=4 sw=4

And then I asked Grok about setting up postgres properly, because I likely goofed that up also.

**Portscout uses PostgreSQL (recommended) or SQLite** to store port metadata, version information, check results, maintainer emails, etc. For a local/unofficial ports tree, the PostgreSQL setup is essentially the same as for the official tree — you just point it at your custom `ports dir`.

### 1. Install PostgreSQL on FreeBSD

```sh
pkg install postgresql17-server postgresql17-client
sysrc postgresql_enable=YES
service postgresql initdb
service postgresql start
```

(Adjust version number if you prefer a different one.)

### 2. Create the Portscout Database User and Database

As root or with `sudo`:

```sh
# Create the user (prompts for a password)
createuser -U postgres -P -d portscout   # -d allows it to create databases if needed

# Create the database
createdb -U postgres -O portscout portscout
```

### 3. Initialize the Schema

Portscout ships with the SQL script:

```sh
psql -U portscout -d portscout -f /usr/local/share/portscout/sql/pgsql_init.sql
```

(If installed via ports/pkg, the path is usually under `/usr/local/share/portscout/sql/`.)

### 4. Configure portscout for PostgreSQL

Edit (or copy from sample) `/usr/local/etc/portscout.conf` (or wherever your config lives):

```conf
# Database settings
db user     = portscout
db name     = portscout
db pass     = your_password_here          # Leave empty if using trust/peer auth

db host     = localhost                   # or 127.0.0.1
db port     = 5432

db connstr  = DBI:Pg:dbname=%(db_name);host=%(db_host);port=%(db_port)
# Or simpler if local Unix socket + trust/peer:
# db connstr = DBI:Pg:dbname=%(db_name)
```

**Important options for local ports usage:**

```conf
ports dir = /path/to/my-local-ports     # Your custom tree root

# Optional restrictions (very useful for unofficial ports)
restrict category   = mycategory1,mycategory2
restrict port       = myport1,myport2
restrict maintainer = your@email.com

datasrc = Portscout::DataSrc::Ports
# datasrc opts = type:FreeBSD   # usually not needed

portconfig enable   = true      # Respect PORTSCOUT= hints in Makefiles
freebsdhacks enable = false     # Set to false if your tree isn't FreeBSD-style
```

Other performance tweaks for smaller trees:
- `num children = 4` (or lower)
- `workqueue size = 10`

### 5. Authentication (pg_hba.conf)

For simplicity on a local machine, edit `/var/db/postgres/data/pg_hba.conf` (or equivalent) and add near the top (before other rules):

```
# TYPE  DATABASE    USER        ADDRESS          METHOD
local   portscout   portscout                    trust
host    portscout   portscout   127.0.0.1/32     scram-sha-256   # or md5
```

Then reload:

```sh
service postgresql reload
```

### 6. Test and Populate

```sh
portscout build          # First full parse of your local tree
portscout rebuild        # Later incremental updates
portscout check          # Check for new upstream versions
portscout showupdates    # See results
```

### Notes for Local/Unofficial Ports

- Your local tree must have a standard structure (`category/port/Makefile` etc.) so the `Ports` data source can walk it.
- `build`/`rebuild` will call `make` in each port directory, so your Makefiles must be valid.
- PostgreSQL gives much better performance than SQLite, especially if you ever scale beyond a handful of ports.
- You can run multiple independent Portscout setups (different config files + different DB names) for official vs. local ports.

For full details, see the installed documentation:

- `perldoc portscout`
- `/usr/local/share/doc/portscout/` or the GitHub repo (portscout.pod and portscout.conf).

This setup works cleanly with your local ports tree while leveraging PostgreSQL for reliable storage and querying of version data.

The results were much much more positive.

# service postgresql initdb
initdb postgresql
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with this locale configuration:
  locale provider:   libc
  LC_COLLATE:  C
  LC_CTYPE:    C.UTF-8
  LC_MESSAGES: C.UTF-8
  LC_MONETARY: C.UTF-8
  LC_NUMERIC:  C.UTF-8
  LC_TIME:     C.UTF-8
The default text search configuration will be set to "english".

Data page checksums are enabled.

creating directory /var/db/postgres/data18 ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default "max_connections" ... 100
selecting default "shared_buffers" ... 128MB
selecting default time zone ... America/Chicago
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    /usr/local/bin/pg_ctl -D /var/db/postgres/data18 -l logfile start

# service postgresql start
start postgresql
# createuser -U postgres -P -d portscout
Enter password for new role: 
Enter it again: 
# createdb -U postgres -O portscout portscout
# psql -U portscout -d portscout -f /usr/local/share/portscout/sql/pgsql_init.sql
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 1
INSERT 0 1
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
# 

My unofficial ports are not arranged in a tree form, they're only a bunch of directories in a main directory GitRepos.  For portscout to work properly, it uses make for its tasks, so I need to setup a PortsTree directory with all the directories named similarly and with all the Makefiles in each directory as well.  This is a simple thing to do, I copy and revise what exists in /usr/ports and each category directory.

I need to edit a few more things.

root@ichigo:~ # vi /var/db/postgres/data18/pg_hba.conf

# TYPE  DATABASE        USER            ADDRESS                 METHOD

local   portscout   portscout                    trust
host    portscout   portscout   127.0.0.1/32     scram-sha-256   # or md5

# "local" is for Unix domain socket connections only
local   all             all                                     trust
# IPv4 local connections:
host    all             all             127.0.0.1/32            trust
# IPv6 local connections:
host    all             all             ::1/128                 trust
# Allow replication connections from localhost, by a user with the
# replication privilege.
/var/db/postgres/data18/pg_hba.conf: 129 lines, 5851 characters
root@ichigo:~ #
After my edits I had to redo a few things.
root@ichigo:~ # service postgresql start
start postgresql
pg_ctl: another server might be running; trying to start server anyway
pg_ctl: could not start server
Examine the log output.
root@ichigo:~ # service postgresql initdb
initdb postgresql
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with this locale configuration:
  locale provider:   libc
  LC_COLLATE:  C
  LC_CTYPE:    C.UTF-8
  LC_MESSAGES: C.UTF-8
  LC_MONETARY: C.UTF-8
  LC_NUMERIC:  C.UTF-8
  LC_TIME:     C.UTF-8
The default text search configuration will be set to "english".

Data page checksums are enabled.

initdb: error: directory "/var/db/postgres/data18" exists but is not empty
initdb: hint: If you want to create a new database system, either remove or empty the directory "/var/db/postgres/data18" or run initdb with an argument other than "/var/db/postgres/data18".
root@ichigo:~ #

That error I tried to solve but saw more error messages instead.  I was working in multiple xterm windows and may have erased /var/db/postgres/data18 at some point to start over and did all of this a number of times.  When those directories are found during the portscout build process logged below, I had constructed a sort of ports tree with the assorted Makefiles in each directory.

tigersharke@ichigo [~/GitRepos] % psql -U portscout -d portscout -f /usr/local/share/portscout/sql/pgsql_init.sql                           
psql: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed: FATAL:  role "portscout" does not exist                            
tigersharke@ichigo [~/GitRepos] % createuser -U postgres -P -d portscout                                                                    
Enter password for new role:                                                                                                                
Enter it again:                                                                                                                             
tigersharke@ichigo [~/GitRepos] % createdb -U postgres -O portscout portscout
tigersharke@ichigo [~/GitRepos] % portscout build
portscout v0.8.1, by Shaun Amott

DBD::Pg::st execute failed: ERROR:  relation "portscout" does not exist
LINE 2:      FROM portscout
                  ^ at /usr/local/lib/perl5/site_perl/Portscout/Util.pm line 856.
DBD::Pg::st fetchrow_array failed: no statement executing at /usr/local/lib/perl5/site_perl/Portscout/Util.pm line 858.
Database schema mismatch; did you forget to upgrade?
tigersharke@ichigo [~/GitRepos] % psql -U portscout -d portscout -f /usr/local/share/portscout/sql/pgsql_init.sql
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 1
INSERT 0 1
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
tigersharke@ichigo [~/GitRepos] % portscout build
portscout v0.8.1, by Shaun Amott

-- [ Building ports database ] -----------------------------------------

Couldn't stat MOVED file at /usr/local/lib/perl5/site_perl/Portscout/DataSrc/Ports.pm line 220.
tigersharke@ichigo [~/GitRepos] % portscout build
portscout v0.8.1, by Shaun Amott

-- [ Building ports database ] -----------------------------------------

Scanning deskutils ...
Matched: deskutils/flameshot-dev
Scanning games ...
Matched: games/endless-sky-dev
Matched: games/minetestmapper-dev
Matched: games/luanti-dev
Matched: games/endless-sky-high-dpi-dev
Matched: games/lutris-freebsd
Scanning graphics ...
Matched: graphics/feh-dev
Scanning www ...
Matched: www/librewolf-dev
Scanning x11-fonts ...
Matched: x11-fonts/google-fonts-dev
Scanning x11-wm ...
Matched: x11-wm/fvwm3-dev

Building...

[deskutils      ] [flameshot-dev                 ] (got 1 out of 10)
make failed for /home/tigersharke/GitRepos/PortsTree/deskutils/flameshot-dev at /usr/local/lib/perl5/site_perl/Portscout/Make.pm line 161.
[games          ] [endless-sky-dev               ] (got 2 out of 10)
make failed for /home/tigersharke/GitRepos/PortsTree/games/endless-sky-dev at /usr/local/lib/perl5/site_perl/Portscout/Make.pm line 161.
[games          ] [minetestmapper-dev            ] (got 3 out of 10)
make failed for /home/tigersharke/GitRepos/PortsTree/games/minetestmapper-dev at /usr/local/lib/perl5/site_perl/Portscout/Make.pm line 161.
[games          ] [luanti-dev                    ] (got 4 out of 10)
make failed for /home/tigersharke/GitRepos/PortsTree/games/luanti-dev at /usr/local/lib/perl5/site_perl/Portscout/Make.pm line 161.
[games          ] [endless-sky-high-dpi-dev      ] (got 5 out of 10)
make failed for /home/tigersharke/GitRepos/PortsTree/games/endless-sky-high-dpi-dev at /usr/local/lib/perl5/site_perl/Portscout/Make.pm line 161.
[games          ] [lutris-freebsd                ] (got 6 out of 10)
make failed for /home/tigersharke/GitRepos/PortsTree/games/lutris-freebsd at /usr/local/lib/perl5/site_perl/Portscout/Make.pm line 161.
[graphics       ] [feh-dev                       ] (got 7 out of 10)
make failed for /home/tigersharke/GitRepos/PortsTree/graphics/feh-dev at /usr/local/lib/perl5/site_perl/Portscout/Make.pm line 161.
[www            ] [librewolf-dev                 ] (got 8 out of 10)
Insufficient data for port www/librewolf-dev: missing version
[x11-fonts      ] [google-fonts-dev              ] (got 9 out of 10)
make failed for /home/tigersharke/GitRepos/PortsTree/x11-fonts/google-fonts-dev at /usr/local/lib/perl5/site_perl/Portscout/Make.pm line 161.
[x11-wm         ] [fvwm3-dev                     ] (got 10 out of 10)
make failed for /home/tigersharke/GitRepos/PortsTree/x11-wm/fvwm3-dev at /usr/local/lib/perl5/site_perl/Portscout/Make.pm line 161.

Cross-referencing master/slave ports...
Processing MOVED entries...
Finalising pending MOVED terminations...
tigersharke@ichigo [~/GitRepos] %

Since this is not the FreeBSD ports tree, portscout doesn't know that I likely would not have a MOVED file, so I simply touch MOVED in the PortsTree directory to satisfy that.  Maybe there is a method to properly generate a MOVED file, I do not know.  The output above seems to indicate that I have to improve my Makefiles, so I will see what portlint tells me, its likely something easy to cure.  The main issue was DISABLE_LICENSES="YES" in /etc/make.conf and the rest were improvements to Makefiles and other files that portlint helped me implement.

The result is now something that looks like it will help me in the future, but I can test now by revising luanti-dev to an older commit, reverting the distinfo file to match.

tigersharke@ichigo [~/GitRepos/PortsTree] % portscout rebuild
portscout v0.8.1, by Shaun Amott

Incremental build: Looking for updated ports...

Scanning deskutils ...
Scanning deskutils/flameshot-dev ... Matched: deskutils/flameshot-dev
Scanning games ...
Scanning games/endless-sky-dev ... Matched: games/endless-sky-dev
Scanning games/minetestmapper-dev ... Matched: games/minetestmapper-dev
Scanning games/luanti-dev ... Scanning games/endless-sky-high-dpi-dev ... Scanning games/lutris-freebsd ... Matched: games/lutris-freebsd
Scanning graphics ...
Scanning graphics/feh-dev ... Matched: graphics/feh-dev
Scanning www ...
Scanning www/librewolf-dev ... Scanning x11-fonts ...
Scanning x11-fonts/google-fonts-dev ... Scanning x11-wm ...
Scanning x11-wm/fvwm3-dev ... 
Building...

[deskutils      ] [flameshot-dev                 ] (got 1 out of 5)
[games          ] [endless-sky-dev               ] (got 2 out of 5)
[games          ] [minetestmapper-dev            ] (got 3 out of 5)
[games          ] [lutris-freebsd                ] (got 4 out of 5)
[graphics       ] [feh-dev                       ] (got 5 out of 5)

Cross-referencing master/slave ports...
tigersharke@ichigo [~/GitRepos/PortsTree] %

Since all of this looks like it is setup and ready, I try portscout check to see what it thinks has updated.

root@ichigo:/home/tigersharke/GitRepos/PortsTree # portscout check
portscout v0.8.1, by Shaun Amott

Couldn't determine GID from name portscout
root@ichigo:/home/tigersharke/GitRepos/PortsTree # exit

That is not a very helpful message as it seems to work fine if I am not root so perhaps warn of user being root instead?  I figured it out anyway.

tigersharke@ichigo [~/GitRepos/PortsTree/games/luanti-dev] % portscout
Usage: 
       portscout build
       portscout rebuild
       portscout check
       portscout uncheck

       portscout mail
       portscout generate
       portscout showupdates

       portscout add-mail user@host ...
       portscout remove-mail user@host ...
       portscout show-mail
tigersharke@ichigo [~/GitRepos/PortsTree/games/luanti-dev] % portscout check
portscout v0.8.1, by Shaun Amott

-- [ Checking ports distfiles ] ----------------------------------------

Spawned PID #14150 (0 ports unallocated)
[lutris-freebsd                ] VersionCheck()
[lutris-freebsd                ] Checking site: https://codeload.github.com/lutris/lutris/tar.gz/0c93c2fad546e684346c8fe3179ab797d5e033f3?dummy=/                                                                                                       
Does site handler exist ... Yes 
[lutris-freebsd                ] Done
[feh-dev                       ] VersionCheck()
[feh-dev                       ] Checking site: https://codeload.github.com/derf/feh/tar.gz/c5e05ad638e819f0f9852df9b33400ca63662d19?dummy=/                                                                                                            
Does site handler exist ... Yes 
[feh-dev                       ] [https://code...3662d19?dummy=/] UPDATE g20260507 -> 3.12.2
[feh-dev                       ] Done
[endless-sky-dev               ] VersionCheck()
[endless-sky-dev               ] Checking site: https://codeload.github.com/endless-sky/endless-sky/tar.gz/2d14be69937685d9b52df1ea3270cacab1f1cfcc?dummy=/                                                                                             
Does site handler exist ... Yes 
[endless-sky-dev               ] Done
[minetestmapper-dev            ] VersionCheck()
[minetestmapper-dev            ] Checking site: https://codeload.github.com/luanti-org/minetestmapper/tar.gz/b5d41a35c388db377edda60c01969fe0fa113ca4?dummy=/                                                                                           
Does site handler exist ... Yes 
[minetestmapper-dev            ] Done
[flameshot-dev                 ] VersionCheck()
[flameshot-dev                 ] Checking site: https://codeload.github.com/flameshot-org/flameshot/tar.gz/a6694bf45ace6a8552351d5f9ca6f006786ce6f9?dummy=/                                                                                             
Does site handler exist ... Yes 
[flameshot-dev                 ] Done
PID #30712 finished work block (took 6 seconds)
Master process finished. All work has been distributed.
tigersharke@ichigo [~/GitRepos/PortsTree/games/luanti-dev] %

I believe feh-dev is actually current to the most recent commit, so I may need to adjust something but setting luanti-dev to use an older commit might help me with additional feedback.  After the adjustment to luanti-dev I need to do portscout rebuild for it to see that something changed locally and then the portscout check will give me a result.

tigersharke@ichigo [~/GitRepos/PortsTree/games/luanti-dev] % portscout rebuild
portscout v0.8.1, by Shaun Amott

Incremental build: Looking for updated ports...

Scanning deskutils ...
Scanning deskutils/flameshot-dev ... Scanning games ...
Scanning games/endless-sky-dev ... Scanning games/minetestmapper-dev ... Scanning games/luanti-dev ... Matched: games/luanti-dev
Scanning games/endless-sky-high-dpi-dev ... Scanning games/lutris-freebsd ... Scanning graphics ...
Scanning graphics/feh-dev ... Scanning www ...
Scanning www/librewolf-dev ... Scanning x11-fonts ...
Scanning x11-fonts/google-fonts-dev ... Scanning x11-wm ...
Scanning x11-wm/fvwm3-dev ... 
Building...

[games          ] [luanti-dev                    ] (got 1 out of 1)

Cross-referencing master/slave ports...
tigersharke@ichigo [~/GitRepos/PortsTree/games/luanti-dev] % portscout check
portscout v0.8.1, by Shaun Amott

-- [ Checking ports distfiles ] ----------------------------------------

Spawned PID #57294 (0 ports unallocated)
[luanti-dev                    ] VersionCheck()
[luanti-dev                    ] Checking site: https://codeload.github.com/luanti-org/luanti/tar.gz/bb7fcd32c7da2f5610458e679f567dbc09a1c909?dummy=/                                                                                                   
Does site handler exist ... Yes 
[luanti-dev                    ] [https://code...9a1c909?dummy=/] UPDATE g20260502 -> 5.16.0
[luanti-dev                    ] Done
[minetestmapper-dev            ] VersionCheck()
[minetestmapper-dev            ] Checking site: https://codeload.github.com/luanti-org/minetestmapper/tar.gz/b5d41a35c388db377edda60c01969fe0fa113ca4?dummy=/                                                                                           
Does site handler exist ... Yes 
[minetestmapper-dev            ] Done
[feh-dev                       ] VersionCheck()
[feh-dev                       ] Checking site: https://codeload.github.com/derf/feh/tar.gz/c5e05ad638e819f0f9852df9b33400ca63662d19?dummy=/                                                                                                            
Does site handler exist ... Yes 
[feh-dev                       ] Done
[lutris-freebsd                ] VersionCheck()
[lutris-freebsd                ] Checking site: https://codeload.github.com/lutris/lutris/tar.gz/0c93c2fad546e684346c8fe3179ab797d5e033f3?dummy=/                                                                                                       
Does site handler exist ... Yes 
[lutris-freebsd                ] Done
[flameshot-dev                 ] VersionCheck()
[flameshot-dev                 ] Checking site: https://codeload.github.com/flameshot-org/flameshot/tar.gz/a6694bf45ace6a8552351d5f9ca6f006786ce6f9?dummy=/                                                                                             
Does site handler exist ... Yes 
[flameshot-dev                 ] Done
[endless-sky-dev               ] VersionCheck()
[endless-sky-dev               ] Checking site: https://codeload.github.com/endless-sky/endless-sky/tar.gz/2d14be69937685d9b52df1ea3270cacab1f1cfcc?dummy=/                                                                                             
Does site handler exist ... Yes 
[endless-sky-dev               ] Done
PID #71632 finished work block (took 6 seconds)
Master process finished. All work has been distributed.
tigersharke@ichigo [~/GitRepos/PortsTree/games/luanti-dev] % portscout
Usage: 
       portscout build
       portscout rebuild
       portscout check
       portscout uncheck

       portscout mail
       portscout generate
       portscout showupdates

       portscout add-mail user@host ...
       portscout remove-mail user@host ...
       portscout show-mail
tigersharke@ichigo [~/GitRepos/PortsTree/games/luanti-dev] % portscout showupdates
portscout v0.8.1, by Shaun Amott

nope@nothere's ports:
  games/luanti-dev g20260502 -> 5.16.0
  graphics/feh-dev g20260507 -> 3.12.2
tigersharke@ichigo [~/GitRepos/PortsTree/games/luanti-dev] %

I know that portscout is technically correct, both luanti-dev and feh-dev have updates, or versions that do not match what I use due to following upstream commits.  There is surely a way to handle this, and maybe the answer is in the ports tree with another bleeding edge variety that follows upstream commits.

I didn't need to edit an xml file, the configuration I ultimately used did not involve one.  Some of the commands Grok gave me seem to have been more successful than those from documentation. It is important to do some tasks as root and some, especially directly interacting with portscout as a user not root.  There also seem to be changes outside of portscout that I had to adjust for that were not in the documentation I was trying to follow.

Looking deeper into portscout confirms for me that there is no method to compare commit hashes as a basis to determine an upstream update.  Grok suggested to use tags but those also tend to be normal version numbers and not tied to commits either.  I asked how I could extend portscout to handle commit hashes, and maybe this is possible but I suspect I would be relying heavily upon how Grok suggests to do it since I know approximately zero about perl.

**An extension to portscout’s GitHub/GitLab site handlers would be a new Perl module** that follows the existing pattern used by `Portscout::SiteHandler::GitHub`, `::PyPI`, and `::SourceForge`.

### Architecture Overview

Site handlers are auto-registered Perl classes in `Portscout/SiteHandler/`. Key points from the core:

- `Portscout::SiteHandler.pm` maintains `@sitehandlers` and has `FindHandler()` which calls `CanHandle()` on each.
- Each handler must:
  - `push @Portscout::SiteHandler::sitehandlers, __PACKAGE__;`
  - Implement `new()`, `CanHandle($url)`, and `GetFiles($url, \%port, \@files)`.
- `GetFiles()` populates the `@$files` array with candidate distfile URLs (or version strings) that the main engine later compares against the port’s current `DISTVERSION`.

The **GitHub handler** currently:
- Detects codeload / archive URLs.
- Calls the GitHub API (`/repos/.../releases` then falls back to `/tags`).
- Collects `tarball_url`s from non-draft/non-prerelease releases (or tags).

### What a Commit/Branch-Aware Extension Could Look Like

For your use case (tracking latest commit on a branch instead of tagged versions), you could create something like `Portscout::SiteHandler::GitHubCommits` or enhance the existing one.

#### Example: `Portscout/SiteHandler/GitHubCommits.pm`

```perl
#------------------------------------------------------------------------------
package Portscout::SiteHandler::GitHubCommits;

use JSON qw(decode_json);
use LWP::UserAgent;
use URI::Escape;

use Portscout::Const;
use Portscout::Config;

use strict;

push @Portscout::SiteHandler::sitehandlers, __PACKAGE__;

our %settings;

sub new {
    my $self = {};
    bless($self, shift);
    $self->{name} = 'GitHubCommits';
    return $self;
}

# CanHandle: Trigger on ports that want commit tracking
# You could key off PORTSCOUT= commit:branchname or a specific URL pattern
sub CanHandle {
    my ($self, $url) = @_;
    # Example patterns or check $port->{portscout} hints
    return ($url =~ /github\.com/ && $url =~ /\/(archive|tar\.gz)/);
    # Or more specific: look for a custom PORTSCOUT flag parsed earlier
}

sub GetFiles {
    my ($self, $url, $port, $files) = @_;

    my $owner_repo = _extract_owner_repo($url) or return 0;
    my $branch = $port->{portscout_branch} || 'main';  # Could come from PORTSCOUT= or Makefile

    # Get latest commit on branch
    my $data = _call_github_api("/repos/$owner_repo/commits/$branch") or return 0;

    if (my $sha = $data->{sha}) {
        # Construct a tarball URL using the commit hash
        my $tarball_url = "https://github.com/$owner_repo/archive/$sha.tar.gz";
        push @$files, $tarball_url;

        # Optionally also store the short SHA or date for version comparison
        _debug("Found latest commit $sha on $branch");
    }

    return 1;
}

# Reuse or copy the _call_github_api from GitHub.pm (support token, etc.)
sub _call_github_api { ... }   # identical to the one in GitHub.pm

sub _extract_owner_repo { ... }

sub _debug { ... }

1;
```

### Integration & Configuration Ideas

1. **Makefile-level hints** (best approach):
   ```make
   PORTSCOUT= site:github-commits branch:main   # or "commit:main"
   # Or
   PORTSCOUT= limit:^g[0-9]{8} site:https://api.github.com/repos/user/repo/commits/main
   ```

2. Extend `Portscout::Make` or the port parsing logic to extract extra fields like `portscout_branch` from the `PORTSCOUT` variable.

3. **Version comparison logic**:
   - For commit tracking you often want to compare SHA or commit date rather than semver.
   - You might need to extend the core comparison (in `Portscout.pm` or wherever versions are ranked) to understand “newer commit” semantics when a handler flags it.

4. **GitLab equivalent** would be almost identical, just using `api.gitlab.com` or self-hosted endpoints (`/projects/.../repository/commits`).

### Practical Steps to Implement

- Copy `GitHub.pm` as a starting point.
- Place the new `.pm` in `/usr/local/lib/perl5/site_perl/Portscout/SiteHandler/` (or wherever portscout installs modules).
- Register it by ensuring the `push` statement runs (it happens at `use`/`require` time).
- Test with `portscout -d rebuild` and `portscout -d check` on a sample port.
- Handle rate limits (GitHub token in `portscout.conf` already exists).

This kind of extension is relatively straightforward because the design is already pluggable. Many people have extended portscout (or its predecessor portroach) this way in the past.

If you want a full “latest commit on default branch” mode, the handler above (plus small core tweaks for version extraction) would get you most of the way there. Would you like a more complete skeleton or help with the `PORTSCOUT` parsing part?

Many may suggest that using AI such as Grok to figure something out is not the best way to get the solution.  I say that if documentation is sparse or spread out or nonexistent, then maybe a tool that has scraped places old and new that also may not be accessible or known could help.  If only to point toward solutions or sources of solutions, using AI will be helpful and speed a solution.

Surely portscout documentation could use some improvement.  There might be some things that are left out of the manpage and documentation, or that have changed since it was originally written.  I believe that extending portscout to check for upstream commits (hashes) instead of version strings or tags (also versions) would be a very useful and helpful addition to the tool.

Right now I have portscout setup to handle all my unofficial ports but it will not tell me anything has updated until a new version rather than a new commit has been made.  All of my unofficial ports get updated based upon upstream commits.  Why should I have to pair portscout with another tool if it could be a one-stop solution to knowing whether any of my growing list of unofficial ports has an upstream commit.  Six is already becoming a challenge, so I can imagine how helpful it is for porters who have more than double or triple that number of ports that are updated based upon upstream release versions.

I tried to use portscout to help me but I mostly learned how to install and configure it properly despite my struggle with the documentation, it will not yet do what I need.

Frequently viewed this week