From 852ad193f0f42f5928c2984cb22127fa4dbe12bf Mon Sep 17 00:00:00 2001
From: "kepler155c@gmail.com" <kepler155c@gmail.com>
Date: Wed, 11 Oct 2017 11:37:52 -0400
Subject: [PATCH] simplify ui

---
 sys/apis/class.lua     |   5 +-
 sys/apis/ui.lua        | 377 +++++++++++++----------------------------
 sys/apis/ui/canvas.lua |  10 +-
 sys/apps/Lua.lua       |   2 +-
 sys/apps/Network.lua   |  10 +-
 sys/apps/Overview.lua  |  20 +--
 sys/apps/System.lua    |  56 +++++-
 sys/apps/multishell    |   7 +
 sys/apps/trust.lua     |   4 +-
 sys/etc/app.db         |   2 +-
 sys/services/chat.lua  |  58 +++++++
 11 files changed, 260 insertions(+), 291 deletions(-)
 create mode 100644 sys/services/chat.lua

diff --git a/sys/apis/class.lua b/sys/apis/class.lua
index 4b5ed06..0466bc8 100644
--- a/sys/apis/class.lua
+++ b/sys/apis/class.lua
@@ -1,8 +1,6 @@
 -- From http://lua-users.org/wiki/SimpleLuaClasses
 -- (with some modifications)
 
-local uid = 1
-
 -- class.lua
 -- Compatible with Lua 5.1 (not 5.0).
 return function(base)
@@ -21,8 +19,7 @@ return function(base)
   -- expose a constructor which can be called by <classname>(<args>)
   setmetatable(c, {
     __call = function(class_tbl, ...)
-      local obj = { __uid = uid }
-      uid = uid + 1
+      local obj = { }
       setmetatable(obj,c)
       if class_tbl.init then
         class_tbl.init(obj, ...)
diff --git a/sys/apis/ui.lua b/sys/apis/ui.lua
index 2f1ee9f..389c1a0 100644
--- a/sys/apis/ui.lua
+++ b/sys/apis/ui.lua
@@ -4,10 +4,16 @@ local Event      = require('event')
 local Transition = require('ui.transition')
 local Util       = require('util')
 
-local _rep   = string.rep
-local _sub   = string.sub
-local colors = _G.colors
-local keys   = _G.keys
+local _rep       = string.rep
+local _sub       = string.sub
+local clipboard  = _G.clipboard
+local colors     = _G.colors
+local device     = _G.device
+local fs         = _G.fs
+local keys       = _G.keys
+local multishell = _ENV.multishell
+local os         = _G.os
+local term       = _G.term
 
 --[[
   Using the shorthand window definition, elements are created from
@@ -35,12 +41,6 @@ local function getPosition(element)
   return x, y
 end
 
-local function assertElement(el, msg)
-  if not el or not type(el) == 'table' or not el.UIElement then
-    error(msg, 3)
-  end
-end
-
 --[[-- Top Level Manager --]]--
 local Manager = class()
 function Manager:init()
@@ -232,11 +232,10 @@ function Manager:configure(appName, ...)
     if not dev then
       error('Invalid display device')
     end
-    local device = self.Device({
+    self:setDefaultDevice(self.Device({
       device = dev,
       textScale = defaults.device.textScale,
-    })
-    self:setDefaultDevice(device)
+    }))
   end
 
   if defaults.theme then
@@ -273,7 +272,6 @@ function Manager:emitEvent(event)
 end
 
 function Manager:inputEvent(parent, event)
-
   while parent do
     if parent.accelerators then
       local acc = parent.accelerators[event.key]
@@ -376,9 +374,9 @@ function Manager:click(button, x, y)
   end
 end
 
-function Manager:setDefaultDevice(device)
-  self.defaultDevice = device
-  self.term = device
+function Manager:setDefaultDevice(dev)
+  self.defaultDevice = dev
+  self.term = dev
 end
 
 function Manager:addPage(name, page)
@@ -490,8 +488,8 @@ function Manager:setProperties(obj, args)
   end
 end
 
-function Manager:dump(el)
-  if el then
+function Manager:dump(inEl)
+  if inEl then
     local function clean(el)
       local o = el
       el = Util.shallowCopy(el)
@@ -511,7 +509,7 @@ function Manager:dump(el)
 
       return el
     end
-    return clean(el)
+    return clean(inEl)
   end
 end
 
@@ -519,6 +517,7 @@ local UI = Manager()
 
 --[[-- Basic drawable area --]]--
 UI.Window = class()
+UI.Window.uid = 1
 UI.Window.defaults = {
   UIElement = 'Window',
   x = 1,
@@ -528,12 +527,46 @@ UI.Window.defaults = {
   offy = 0,
   cursorX = 1,
   cursorY = 1,
-  -- accelerators = { },
 }
 function UI.Window:init(args)
-  local defaults = UI:getDefaults(UI.Window, args)
+  -- merge defaults for all subclasses
+  local defaults = args
+  local m = self
+  repeat
+    defaults = UI:getDefaults(m, defaults)
+    m = m._base
+  until not m
   UI:setProperties(self, defaults)
+
+  -- each element has a unique ID
+  self.uid = UI.Window.uid
+  UI.Window.uid = UI.Window.uid + 1
+
+  -- at this time, the object has all the properties set
+
+  -- postInit is a special constructor. the element does not need to implement
+  -- the method. But we need to guarantee that each subclass which has this
+  -- method is called.
+  m = self
+  local lpi
+  repeat
+    if m.postInit and m.postInit ~= lpi then
+--debug('calling ' .. m.defaults.UIElement)
+--debug(rawget(m, 'postInit'))
+      m.postInit(self)
+      lpi = m.postInit
+--    else
+--debug('skipping ' .. m.defaults.UIElement)
+--debug(rawget(m, 'postInit'))
+    end
+    m = m._base
+  until not m
+end
+
+function UI.Window:postInit()
   if self.parent then
+    -- this will cascade down the whole tree of elements starting at the
+    -- top level window (which has a device as a parent)
     self:setParent()
   end
 end
@@ -749,7 +782,7 @@ function UI.Window:print(text, bg, fg)
     if #result > 1 and result[2] > cx then
       return _sub(line, cx, result[2] + 1)
     elseif #result > 0 and result[1] == cx then
-      result = { line:find("(%w+)", result[2] + 1) }
+      result = { line:find("(%w+)", result[2]) }
       if #result > 0 then
         return _sub(line, cx, result[1] + 1)
       end
@@ -811,10 +844,10 @@ function UI.Window:print(text, bg, fg)
           if self.cursorX + #word > width then
             self.cursorX = marginLeft + 1
             self.cursorY = self.cursorY + 1
-            w = word:gsub(' ', '')
+            w = word:gsub('^ ', '')
           end
           self:write(self.cursorX, self.cursorY, w, bg, fg)
-          self.cursorX = self.cursorX + #word
+          self.cursorX = self.cursorX + #w
           lx = lx + #word
         end
       end
@@ -829,7 +862,6 @@ function UI.Window:print(text, bg, fg)
 end
 
 function UI.Window:setFocus(focus)
-  assertElement(focus, 'UI.Window:setFocus: Invalid element passed')
   if self.parent then
     self.parent:setFocus(focus)
   end
@@ -864,7 +896,6 @@ function UI.Window:getFocusables()
 end
 
 function UI.Window:focusFirst()
-
   local focusables = self:getFocusables()
   local focused = focusables[1]
   if focused then
@@ -904,7 +935,6 @@ end
 
 function UI.Window:addLayer(bg, fg)
   local canvas = self:getCanvas()
-
   return canvas:addLayer(self, bg, fg)
 end
 
@@ -947,23 +977,19 @@ UI.Device.defaults = {
   textScale = 1,
   effectsEnabled = true,
 }
-function UI.Device:init(args)
-  local defaults = UI:getDefaults(UI.Device)
-  defaults.device = term.current()
-  UI:setProperties(defaults, args)
+function UI.Device:postInit()
+  self.device = self.device or term.current()
 
-  if defaults.deviceType then
-    defaults.device = device[defaults.deviceType]
+  if self.deviceType then
+    self.device = device[self.deviceType]
   end
 
-  if not defaults.device.setTextScale then
-    defaults.device.setTextScale = function() end
+  if not self.device.setTextScale then
+    self.device.setTextScale = function() end
   end
 
-  defaults.device.setTextScale(defaults.textScale)
-  defaults.width, defaults.height = defaults.device.getSize()
-
-  UI.Window.init(self, defaults)
+  self.device.setTextScale(self.textScale)
+  self.width, self.height = self.device.getSize()
 
   self.isColor = self.device.isColor()
 
@@ -979,7 +1005,6 @@ function UI.Device:resize()
   self.lines = { }
   self.canvas:resize(self.width, self.height)
   self.canvas:clear(self.backgroundColor, self.textColor)
-  --UI.Window.resize(self)
 end
 
 function UI.Device:setCursorPos(x, y)
@@ -1029,7 +1054,6 @@ function UI.Device:addTransition(effect, args)
 end
 
 function UI.Device:runTransitions(transitions, canvas)
-
   for _,t in ipairs(transitions) do
     canvas:punch(t.args)               -- punch out the effect areas
   end
@@ -1051,7 +1075,6 @@ function UI.Device:runTransitions(transitions, canvas)
 end
 
 function UI.Device:sync()
-
   local transitions
   if self.transitions and self.effectsEnabled then
     transitions = self.transitions
@@ -1146,14 +1169,14 @@ UI.Page.defaults = {
   backgroundColor = colors.cyan,
   textColor = colors.white,
 }
-function UI.Page:init(args)
-  local defaults = UI:getDefaults(UI.Page)
-  defaults.parent = UI.defaultDevice
-  UI:setProperties(defaults, args)
-  UI.Window.init(self, defaults)
+function UI.Page:postInit()
+  self.parent = self.parent or UI.defaultDevice
+end
 
+function UI.Page:setParent()
+  UI.Window.setParent(self)
   if self.z then
-    self.canvas = self.parent.canvas:addLayer(self, self.backgroundColor, self.textColor)
+    self.canvas = self:addLayer(self.backgroundColor, self.textColor)
   else
     self.canvas = self.parent.canvas
   end
@@ -1179,7 +1202,6 @@ function UI.Page:getFocused()
 end
 
 function UI.Page:focusPrevious()
-
   local function getPreviousFocus(focused)
     local focusables = self:getFocusables()
     for k, v in ipairs(focusables) do
@@ -1199,7 +1221,6 @@ function UI.Page:focusPrevious()
 end
 
 function UI.Page:focusNext()
-
   local function getNextFocus(focused)
     local focusables = self:getFocusables()
     for k, v in ipairs(focusables) do
@@ -1219,8 +1240,6 @@ function UI.Page:focusNext()
 end
 
 function UI.Page:setFocus(child)
-  assertElement(child, 'UI.Page:setFocus: Invalid element passed')
-
   if not child.focus then
     return
   end
@@ -1287,9 +1306,8 @@ UI.Grid.defaults = {
     [ 'control-f' ] = 'scroll_pageDown',
   },
 }
-function UI.Grid:init(args)
-  local defaults = UI:getDefaults(UI.Grid, args)
-  UI.Window.init(self, defaults)
+function UI.Grid:setParent()
+  UI.Window.setParent(self)
 
   for _,c in pairs(self.columns) do
     c.cw = c.width
@@ -1297,10 +1315,7 @@ function UI.Grid:init(args)
       c.heading = ''
     end
   end
-end
 
-function UI.Grid:setParent()
-  UI.Window.setParent(self)
   self:update()
 
   if not self.pageSize then
@@ -1324,7 +1339,6 @@ function UI.Grid:resize()
 end
 
 function UI.Grid:adjustWidth()
-
   local t = { }        -- cols without width
   local w = self.width - #self.columns - 1 - self.marginRight -- width remaining
 
@@ -1449,7 +1463,6 @@ end
 -- Something about the displayed table has changed
 -- resort the table
 function UI.Grid:update()
-
   local function sort(a, b)
     if not a[self.sortColumn] then
       return false
@@ -1618,7 +1631,6 @@ function UI.Grid:setPage(pageNo)
 end
 
 function UI.Grid:eventHandler(event)
-
   if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
     if not self.disableHeader then
       if event.y == 1 then
@@ -1686,8 +1698,7 @@ UI.ScrollingGrid.defaults = {
   scrollOffset = 0,
   marginRight = 1,
 }
-function UI.ScrollingGrid:init(args)
-  UI.Grid.init(self, UI:getDefaults(UI.ScrollingGrid, args))
+function UI.ScrollingGrid:postInit()
   self.scrollBar = UI.ScrollBar()
 end
 
@@ -1743,12 +1754,9 @@ UI.Menu.defaults = {
   disableHeader = true,
   columns = { { heading = 'Prompt', key = 'prompt', width = 20 } },
 }
-function UI.Menu:init(args)
-  local defaults = UI:getDefaults(UI.Menu)
-  defaults.values = args['menuItems']
-  UI:setProperties(defaults, args)
-  UI.Grid.init(self, defaults)
-  self.pageSize = #args.menuItems
+function UI.Menu:postInit()
+  self.values = self.menuItems
+  self.pageSize = #self.menuItems
 end
 
 function UI.Menu:setParent()
@@ -1813,11 +1821,6 @@ UI.Viewport.defaults = {
     [ 'control-f' ] = 'scroll_pageDown',
   },
 }
-function UI.Viewport:init(args)
-  local defaults = UI:getDefaults(UI.Viewport, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.Viewport:setScrollPosition(offset)
   local oldOffset = self.offy
   self.offy = math.max(offset, 0)
@@ -1849,7 +1852,6 @@ function UI.Viewport:getViewArea()
 end
 
 function UI.Viewport:eventHandler(event)
-
   if event.type == 'scroll_down' then
     self:setScrollPosition(self.offy + 1)
   elseif event.type == 'scroll_up' then
@@ -1868,36 +1870,6 @@ function UI.Viewport:eventHandler(event)
   return true
 end
 
---[[-- ScrollingText --]]--
-UI.ScrollingText = class(UI.Window)
-UI.ScrollingText.defaults = {
-  UIElement = 'ScrollingText',
-  backgroundColor = colors.black,
-  buffer = { },
-}
-function UI.ScrollingText:init(args)
-  local defaults = UI:getDefaults(UI.ScrollingText, args)
-  UI.Window.init(self, defaults)
-end
-
-function UI.ScrollingText:appendLine(text)
-  if #self.buffer+1 >= self.height then
-    table.remove(self.buffer, 1)
-  end
-  table.insert(self.buffer, text)
-end
-
-function UI.ScrollingText:clear()
-  self.buffer = { }
-  UI.Window.clear(self)
-end
-
-function UI.ScrollingText:draw()
-  for k,text in ipairs(self.buffer) do
-    self:write(1, k, Util.widthify(text, self.width), self.backgroundColor)
-  end
-end
-
 --[[-- TitleBar --]]--
 UI.TitleBar = class(UI.Window)
 UI.TitleBar.defaults = {
@@ -1909,11 +1881,6 @@ UI.TitleBar.defaults = {
   frameChar = '-',
   closeInd = '*',
 }
-function UI.TitleBar:init(args)
-  local defaults = UI:getDefaults(UI.TitleBar, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.TitleBar:draw()
   local sb = SB:new(self.width)
   sb:fill(2, self.frameChar, sb.width - 3)
@@ -1960,11 +1927,6 @@ UI.Button.defaults = {
     mouse_click = 'button_activate',
   }
 }
-function UI.Button:init(args)
-  local defaults = UI:getDefaults(UI.Button, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.Button:setParent()
   if not self.width and not self.ex then
     self.width = #self.text + 2
@@ -2017,10 +1979,6 @@ UI.MenuItem.defaults = {
   backgroundFocusColor = colors.lightGray,
 }
 
-function UI.MenuItem:init(args)
-  UI.Button.init(self, UI:getDefaults(UI.MenuItem, args))
-end
-
 --[[-- MenuBar --]]--
 UI.MenuBar = class(UI.Window)
 UI.MenuBar.defaults = {
@@ -2036,25 +1994,8 @@ UI.MenuBar.defaults = {
 }
 UI.MenuBar.spacer = { spacer = true, text = 'spacer', inactive = true }
 
-function UI.MenuBar:init(args)
-  local defaults = UI:getDefaults(UI.MenuBar, args)
-  UI.Window.init(self, defaults)
-
-  if not self.children then
-    self.children = { }
-  end
-
+function UI.MenuBar:postInit()
   self:addButtons(self.buttons)
-  if self.showBackButton then  -- need to remove
-    table.insert(self.children, UI.MenuItem({
-      x = UI.term.width - 2,
-      width = 3,
-      backgroundColor = self.backgroundColor,
-      textColor = self.textColor,
-      text = '^-',
-      event = 'back',
-    }))
-  end
 end
 
 function UI.MenuBar:addButtons(buttons)
@@ -2126,11 +2067,6 @@ UI.DropMenuItem.defaults = {
   textInactiveColor = colors.lightGray,
   backgroundFocusColor = colors.lightGray,
 }
-
-function UI.DropMenuItem:init(args)
-  UI.Button.init(self, UI:getDefaults(UI.DropMenuItem, args))
-end
-
 function UI.DropMenuItem:eventHandler(event)
   if event.type == 'button_activate' then
     self.parent:hide()
@@ -2145,11 +2081,6 @@ UI.DropMenu.defaults = {
   backgroundColor = colors.white,
   buttonClass = 'DropMenuItem',
 }
-function UI.DropMenu:init(args)
-  local defaults = UI:getDefaults(UI.DropMenu, args)
-  UI.MenuBar.init(self, defaults)
-end
-
 function UI.DropMenu:setParent()
   UI.MenuBar.setParent(self)
 
@@ -2180,7 +2111,6 @@ function UI.DropMenu:enable()
 end
 
 function UI.DropMenu:show(x, y)
-
   self.x, self.y = x, y
   self.canvas:move(x, y)
   self.canvas:setVisible(true)
@@ -2226,14 +2156,10 @@ UI.TabBar.defaults = {
   selectedBackgroundColor = colors.cyan,
   focusBackgroundColor = colors.green,
 }
-function UI.TabBar:init(args)
-  local defaults = UI:getDefaults(UI.TabBar, args)
-  UI.MenuBar.init(self, defaults)
-end
-
 function UI.TabBar:selectTab(text)
   local selected, lastSelected
-  for k,child in pairs(self.children) do
+
+  for k,child in pairs(self:getFocusables()) do
     if child.selected then
       lastSelected = k
     end
@@ -2258,23 +2184,19 @@ UI.Tabs = class(UI.Window)
 UI.Tabs.defaults = {
   UIElement = 'Tabs',
 }
-function UI.Tabs:init(args)
-  local defaults = UI:getDefaults(UI.Tabs, args)
-  UI.Window.init(self, defaults)
-
+function UI.Tabs:postInit()
   self:add(self)
 end
 
 function UI.Tabs:add(children)
-
   local buttons = { }
   for _,child in pairs(children) do
-    if type(child) == 'table' and child.UIElement then
+    if type(child) == 'table' and child.UIElement and child.tabTitle then
       child.y = 2
       table.insert(buttons, {
-        text = child.tabTitle or '',
+        text = child.tabTitle,
         event = 'tab_select',
-        tabUid = child.__uid,
+        tabUid = child.uid,
       })
     end
   end
@@ -2294,32 +2216,35 @@ end
 
 function UI.Tabs:enable()
   self.enabled = true
-
-  local _, menuItem = Util.first(self.tabBar.children, function(a, b) return a.x < b.x end)
-  if menuItem then
-    self:activateTab(Util.find(self.children, '__uid', menuItem.tabUid))
-  end
   self.tabBar:enable()
+
+  -- focus first tab
+  local menuItem = self.tabBar:getFocusables()[1]
+  if menuItem then
+    self:activateTab(menuItem.tabUid)
+  end
 end
 
-function UI.Tabs:activateTab(tab)
-  for _,child in ipairs(self.children) do
-    if child ~= self.tabBar then
-      child:disable()
+function UI.Tabs:activateTab(uid)
+  local tab = Util.find(self.children, 'uid', uid)
+  if tab then
+    for _,child in pairs(self.children) do
+      if child.uid ~= uid and child.tabTitle then
+        child:disable()
+      end
+    end
+    if not tab.enabled then
+      self.tabBar:selectTab(tab.tabTitle)
+      tab:enable()
+      tab:draw()
+      self:emit({ type = 'tab_activate', activated = tab, element = self })
     end
   end
-  self.tabBar:selectTab(tab.tabTitle)
-  tab:enable()
-  tab:draw()
-  self:emit({ type = 'tab_activate', activated = tab, element = self })
 end
 
 function UI.Tabs:eventHandler(event)
   if event.type == 'tab_select' then
-    local child = Util.find(self.children, '__uid', event.button.tabUid)
-    if child then
-      self:activateTab(child)
-    end
+    self:activateTab(event.button.tabUid)
   elseif event.type == 'tab_change' then
     for _,tab in ipairs(self.children) do
       if tab ~= self.tabBar then
@@ -2340,11 +2265,6 @@ UI.WindowScroller.defaults = {
   UIElement = 'WindowScroller',
   children = { },
 }
-function UI.WindowScroller:init(args)
-  local defaults = UI:getDefaults(UI.WindowScroller, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.WindowScroller:enable()
   self.enabled = true
   if #self.children > 0 then
@@ -2385,11 +2305,6 @@ UI.Notification.defaults = {
   backgroundColor = colors.gray,
   height = 3,
 }
-function UI.Notification:init(args)
-  local defaults = UI:getDefaults(UI.Notification, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.Notification:draw()
 end
 
@@ -2430,8 +2345,7 @@ function UI.Notification:display(value, timeout)
     self.canvas:removeLayer()
   end
 
-  -- need to get the current canvas - not ui.term.canvas
-  self.canvas = UI.term.canvas:addLayer(self, self.backgroundColor, self.textColor or colors.white)
+  self.canvas = self:addLayer(self.backgroundColor, self.textColor)
   self:addTransition('expandUp', { ticks = self.height })
   self.canvas:setVisible(true)
   self:clear()
@@ -2461,12 +2375,6 @@ UI.Throttle.defaults = {
     '  //)    (O ). @  \\-d )      (@ '
   }
 }
-
-function UI.Throttle:init(args)
-  local defaults = UI:getDefaults(UI.Throttle, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.Throttle:setParent()
   self.x = math.ceil((self.parent.width - self.width) / 2)
   self.y = math.ceil((self.parent.height - self.height) / 2)
@@ -2495,7 +2403,7 @@ function UI.Throttle:update()
     self.c = os.clock()
     self.enabled = true
     if not self.canvas then
-      self.canvas = UI.term.canvas:addLayer(self, self.backgroundColor, colors.cyan)
+      self.canvas = self:addLayer(self.backgroundColor, colors.cyan)
       self.canvas:setVisible(true)
       self:clear(colors.cyan)
     end
@@ -2520,11 +2428,6 @@ UI.StatusBar.defaults = {
   height = 1,
   ey = -1,
 }
-function UI.StatusBar:init(args)
-  local defaults = UI:getDefaults(UI.StatusBar, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.StatusBar:adjustWidth()
   -- Can only have 1 adjustable width
   if self.columns then
@@ -2624,11 +2527,6 @@ UI.ProgressBar.defaults = {
   height = 1,
   value = 0,
 }
-function UI.ProgressBar:init(args)
-  local defaults = UI:getDefaults(UI.ProgressBar, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.ProgressBar:draw()
   self:clear()
   local width = math.ceil(self.value / 100 * self.width)
@@ -2644,11 +2542,6 @@ UI.VerticalMeter.defaults = {
   width = 1,
   value = 0,
 }
-function UI.VerticalMeter:init(args)
-  local defaults = UI:getDefaults(UI.VerticalMeter, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.VerticalMeter:draw()
   local height = self.height - math.ceil(self.value / 100 * self.height)
   self:clear()
@@ -2672,9 +2565,7 @@ UI.TextEntry.defaults = {
     [ 'control-c' ] = 'copy',
   }
 }
-function UI.TextEntry:init(args)
-  local defaults = UI:getDefaults(UI.TextEntry, args)
-  UI.Window.init(self, defaults)
+function UI.TextEntry:postInit()
   self.value = tostring(self.value)
 end
 
@@ -2721,6 +2612,9 @@ function UI.TextEntry:draw()
     if self.scroll and self.scroll > 0 then
       text = text:sub(1 + self.scroll)
     end
+    if self.mask then
+      text = _rep('*', #text)
+    end
   else
     tc = colors.gray
     text = self.shadowText
@@ -2844,15 +2738,9 @@ UI.Chooser.defaults = {
   UIElement = 'Chooser',
   choices = { },
   nochoice = 'Select',
-  --backgroundColor = colors.lightGray,
   backgroundFocusColor = colors.lightGray,
   height = 1,
 }
-function UI.Chooser:init(args)
-  local defaults = UI:getDefaults(UI.Chooser, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.Chooser:setParent()
   if not self.width and not self.ex then
     self.width = 1
@@ -2926,11 +2814,6 @@ UI.Text.defaults = {
   value = '',
   height = 1,
 }
-function UI.Text:init(args)
-  local defaults = UI:getDefaults(UI.Text, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.Text:setParent()
   if not self.width and not self.ex then
     self.width = #tostring(self.value)
@@ -2956,10 +2839,6 @@ UI.ScrollBar.defaults = {
   x = -1,
   ey = -1,
 }
-function UI.ScrollBar:init(args)
-  UI.Window.init(self, UI:getDefaults(UI.ScrollBar, args))
-end
-
 function UI.ScrollBar:draw()
   local parent = self.parent
   local view = parent:getViewArea()
@@ -3000,7 +2879,6 @@ function UI.ScrollBar:draw()
 end
 
 function UI.ScrollBar:eventHandler(event)
-
   if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
     if event.x == 1 then
       local view = self.parent:getViewArea()
@@ -3025,8 +2903,7 @@ UI.TextArea.defaults = {
   marginRight = 2,
   value = '',
 }
-function UI.TextArea:init(args)
-  UI.Viewport.init(self, UI:getDefaults(UI.TextArea, args))
+function UI.TextArea:postInit()
   self.scrollBar = UI.ScrollBar()
 end
 
@@ -3062,9 +2939,7 @@ UI.Form.defaults = {
   margin = 2,
   event = 'form_complete',
 }
-function UI.Form:init(args)
-  local defaults = UI:getDefaults(UI.Form, args)
-  UI.Window.init(self, defaults)
+function UI.Form:postInit()
   self:createForm()
 end
 
@@ -3185,20 +3060,17 @@ UI.Dialog.defaults = {
   textColor = colors.black,
   backgroundColor = colors.white,
 }
-function UI.Dialog:init(args)
-  local defaults = UI:getDefaults(UI.Dialog, args)
-
-  if not defaults.width then
-    defaults.width = UI.term.width-11
-  end
-  defaults.titleBar = UI.TitleBar({ previousPage = true, title = defaults.title })
-  UI.Page.init(self, defaults)
+function UI.Dialog:postInit()
+  self.titleBar = UI.TitleBar({ previousPage = true, title = self.title })
 end
 
 function UI.Dialog:setParent()
-  UI.Window.setParent(self)
+  if not self.width then
+    self.width = self.parent.width - 11
+  end
   self.x = math.floor((self.parent.width - self.width) / 2) + 1
   self.y = math.floor((self.parent.height - self.height) / 2) + 1
+  UI.Page.setParent(self)
 end
 
 function UI.Dialog:disable()
@@ -3226,11 +3098,6 @@ UI.Image.defaults = {
   UIElement = 'Image',
   event = 'button_press',
 }
-function UI.Image:init(args)
-  local defaults = UI:getDefaults(UI.Image, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.Image:setParent()
   if self.image then
     self.height = #self.image
@@ -3270,11 +3137,6 @@ UI.NftImage.defaults = {
   UIElement = 'NftImage',
   event = 'button_press',
 }
-function UI.NftImage:init(args)
-  local defaults = UI:getDefaults(UI.NftImage, args)
-  UI.Window.init(self, defaults)
-end
-
 function UI.NftImage:setParent()
   if self.image then
     self.height = self.image.height
@@ -3286,7 +3148,6 @@ function UI.NftImage:setParent()
 end
 
 function UI.NftImage:draw()
---  self:clear()
   if self.image then
     for y = 1, self.image.height do
       for x = 1, #self.image.text[y] do
diff --git a/sys/apis/ui/canvas.lua b/sys/apis/ui/canvas.lua
index c7d3c9f..943f218 100644
--- a/sys/apis/ui/canvas.lua
+++ b/sys/apis/ui/canvas.lua
@@ -22,8 +22,6 @@ end
 function Canvas:init(args)
   self.x = 1
   self.y = 1
-  self.bg = colors.black
-  self.fg = colors.white
   self.layers = { }
 
   Util.merge(self, args)
@@ -88,7 +86,7 @@ function Canvas:copy()
   return b
 end
 
-function Canvas:addLayer(layer, bg, fg)
+function Canvas:addLayer(layer)
   local canvas = Canvas({
     x       = layer.x,
     y       = layer.y,
@@ -96,8 +94,6 @@ function Canvas:addLayer(layer, bg, fg)
     height  = layer.height,
     isColor = self.isColor,
   })
-  canvas:clear(bg, fg)
-
   canvas.parent = self
   table.insert(self.layers, canvas)
   return canvas
@@ -199,8 +195,8 @@ end
 
 function Canvas:clear(bg, fg)
   local text = _rep(' ', self.width)
-  fg = _rep(self.palette[fg or self.fg], self.width)
-  bg = _rep(self.palette[bg or self.bg], self.width)
+  fg = _rep(self.palette[fg or colors.white], self.width)
+  bg = _rep(self.palette[bg or colors.black], self.width)
   for i = 1, self.height do
     self:writeLine(i, text, fg, bg)
   end
diff --git a/sys/apps/Lua.lua b/sys/apps/Lua.lua
index ceda415..0138b18 100644
--- a/sys/apps/Lua.lua
+++ b/sys/apps/Lua.lua
@@ -204,7 +204,7 @@ function page:setResult(result)
         if Util.size(v) == 0 then
           entry.value = 'table: (empty)'
         else
-          entry.value = 'table'
+          entry.value = tostring(v)
         end
       end
       table.insert(t, entry)
diff --git a/sys/apps/Network.lua b/sys/apps/Network.lua
index a069c51..22b0487 100644
--- a/sys/apps/Network.lua
+++ b/sys/apps/Network.lua
@@ -34,6 +34,7 @@ local page = UI.Page {
         UI.MenuBar.spacer,
         { text = 'Reboot      r', event = 'reboot' },
       } },
+      { text = 'Chat', event = 'chat' },
       { text = 'Trust', dropdown = {
         { text = 'Establish', event = 'trust'   },
         { text = 'Remove',    event = 'untrust' },
@@ -81,7 +82,7 @@ end
 function page:eventHandler(event)
   local t = self.grid:getSelected()
   if t then
-    if event.type == 'telnet' or event.type == 'grid_select' then
+    if event.type == 'telnet' then
       multishell.openTab({
         path = 'sys/apps/telnet.lua',
         focused = true,
@@ -108,6 +109,13 @@ function page:eventHandler(event)
       trustList[t.id] = nil
       Util.writeTable('usr/.known_hosts', trustList)
 
+    elseif event.type == 'chat' then
+      multishell.openTab({
+        path    = 'sys/apps/shell',
+        args    = { 'chat join opusChat-' .. t.id .. ' guest'},
+        title   = 'Chatroom',
+        focused = true,
+      })
     elseif event.type == 'reboot' then
       sendCommand(t.id, 'reboot')
 
diff --git a/sys/apps/Overview.lua b/sys/apps/Overview.lua
index 4d566bf..e492942 100644
--- a/sys/apps/Overview.lua
+++ b/sys/apps/Overview.lua
@@ -118,12 +118,12 @@ local function parseIcon(iconText)
 end
 
 UI.VerticalTabBar = class(UI.TabBar)
-function UI.VerticalTabBar:init(args)
-  UI.TabBar.init(self, args)
+function UI.VerticalTabBar:setParent()
   self.x = 1
   self.width = 8
   self.height = nil
   self.ey = -1
+  UI.TabBar.setParent(self)
   for k,c in pairs(self.children) do
     c.x = 1
     c.y = k + 1
@@ -160,16 +160,11 @@ local page = UI.Page {
 }
 
 UI.Icon = class(UI.Window)
-function UI.Icon:init(args)
-  local defaults = {
-    UIElement = 'Icon',
-    width = 14,
-    height = 4,
-  }
-  UI:setProperties(defaults, args)
-  UI.Window.init(self, defaults)
-end
-
+UI.Icon.defaults = {
+  UIElement = 'Icon',
+  width = 14,
+  height = 4,
+}
 function UI.Icon:eventHandler(event)
   if event.type == 'mouse_click' then
     self:setFocus(self.button)
@@ -498,7 +493,6 @@ function editor:eventHandler(event)
       width = self.width,
       height = self.height,
     })
-    --fileui:setTransition(UI.effect.explode)
     UI:setPage(fileui, fs.getDir(self.iconFile), function(fileName)
       if fileName then
         self.iconFile = fileName
diff --git a/sys/apps/System.lua b/sys/apps/System.lua
index 906dcba..ea2b1ce 100644
--- a/sys/apps/System.lua
+++ b/sys/apps/System.lua
@@ -1,8 +1,10 @@
 _G.requireInjector()
 
-local Config = require('config')
-local UI     = require('ui')
-local Util   = require('util')
+local Config   = require('config')
+local Security = require('security')
+local SHA1     = require('sha1')
+local UI       = require('ui')
+local Util     = require('util')
 
 local fs         = _G.fs
 local multishell = _ENV.multishell
@@ -24,7 +26,7 @@ end
 local env = {
   path = shell.path(),
   aliases = shell.aliases(),
-  lua_path = LUA_PATH,
+  lua_path = _ENV.LUA_PATH,
 }
 Config.load('shell', env)
 
@@ -77,6 +79,36 @@ local systemPage = UI.Page {
       },
     },
 
+    passwordTab = UI.Window {
+      tabTitle = 'Password',
+      oldPass = UI.TextEntry {
+        x = 2, y = 2, ex = -2,
+        limit = 32,
+        mask = true,
+        shadowText = 'old password',
+        inactive = not Security.getPassword(),
+      },
+      newPass = UI.TextEntry {
+        y = 3, x = 2, ex = -2,
+        limit = 32,
+        mask = true,
+        shadowText = 'new password',
+        accelerators = {
+          enter = 'new_password',
+        },
+      },
+      button = UI.Button {
+        x = 2, y = 5,
+        text = 'Update',
+        event = 'update_password',
+      },
+      info = UI.TextArea {
+        x = 2, ex = -2,
+        y = 7,
+        value = 'Add a password to enable other computers to connect to this one.',
+      }
+    },
+
     infoTab = UI.Window {
       tabTitle = 'Info',
       labelText = UI.Text {
@@ -205,6 +237,22 @@ function systemPage.tabs.aliasTab:eventHandler(event)
   end
 end
 
+function systemPage.tabs.passwordTab:eventHandler(event)
+  if event.type == 'update_password' then
+    if #self.newPass.value == 0 then
+      systemPage.notification:error('Invalid password')
+    elseif Security.getPassword() and not Security.verifyPassword(SHA1.sha1(self.oldPass.value)) then
+      systemPage.notification:error('Passwords do not match')
+    else
+      Security.updatePassword(SHA1.sha1(self.newPass.value))
+      self.oldPass.inactive = false
+      systemPage.notification:success('Password updated')
+    end
+
+    return true
+  end
+end
+
 function systemPage.tabs.infoTab:eventHandler(event)
   if event.type == 'update_label' then
     os.setComputerLabel(self.label.value)
diff --git a/sys/apps/multishell b/sys/apps/multishell
index b9ddfb5..aa07d8d 100644
--- a/sys/apps/multishell
+++ b/sys/apps/multishell
@@ -395,6 +395,13 @@ function multishell.hideTab(tabId)
   local tab = tabs[tabId]
   if tab then
     tab.hidden = true
+    if currentTab.tabId == tabId then
+      if tabs[currentTab.previousTabId] then
+        multishell.setFocus(currentTab.previousTabId)
+      else
+        multishell.setFocus(overviewTab.tabId)
+      end
+    end
     redrawMenu()
   end
 end
diff --git a/sys/apps/trust.lua b/sys/apps/trust.lua
index 946ff4d..54e7a29 100644
--- a/sys/apps/trust.lua
+++ b/sys/apps/trust.lua
@@ -29,10 +29,10 @@ if not password then
 end
 
 print('connecting...')
-local socket = Socket.connect(remoteId, 19)
+local socket, msg = Socket.connect(remoteId, 19)
 
 if not socket then
-  error('Unable to connect to ' .. remoteId .. ' on port 19')
+  error(msg)
 end
 
 local publicKey = Security.getPublicKey()
diff --git a/sys/etc/app.db b/sys/etc/app.db
index 1c6a4d8..957a7f3 100644
--- a/sys/etc/app.db
+++ b/sys/etc/app.db
@@ -222,7 +222,7 @@
   [ "d8c298dd41e4a4ec20e8307901797b64688b3b77" ] = {
     title = "GPS Deploy",
     category = "Apps",
-    run = "http://pastebin.com/raw/qLthLak5",
+    run = "http://pastebin.com/raw/VXAyXqBv",
     requires = "turtle",
   },
   [ "53a5d150062b1e03206b9e15854b81060e3c7552" ] = {
diff --git a/sys/services/chat.lua b/sys/services/chat.lua
new file mode 100644
index 0000000..8f273f0
--- /dev/null
+++ b/sys/services/chat.lua
@@ -0,0 +1,58 @@
+local device     = _G.device
+local multishell = _ENV.multishell
+local os         = _G.os
+local parallel   = _G.parallel
+
+if device.wireless_modem then
+
+	multishell.setTitle(multishell.getCurrent(), 'Chat Daemon')
+
+	local tab
+
+	local function chatClient()
+
+	  _G.requireInjector()
+
+		local Event = require('event')
+		local Util  = require('util')
+
+		local h = Event.addRoutine(function()
+			while true do
+				Util.run(_ENV, 'rom/programs/rednet/chat',
+					'join', 'opusChat-' .. os.getComputerID(), 'owner')
+			end
+		end)
+
+		while true do
+			local e = { os.pullEventRaw() }
+			if e[1] == 'terminate' then
+				multishell.hideTab(tab.tabId)
+			else
+				if e[1] == 'rednet_message' and e[4] == 'chat' and e[3].sType == 'chat' then
+					if tab.hidden then
+						multishell.unhideTab(tab.tabId)
+					end
+				end
+				h:resume(unpack(e))
+			end
+		end
+	end
+
+	parallel.waitForAll(
+		function()
+			os.run(_ENV, 'rom/programs/rednet/chat',
+				'host', 'opusChat-' .. os.getComputerID())
+		end,
+		function()
+			os.sleep(3)
+			local tabId = multishell.openTab({
+				fn   = chatClient,
+				title  = 'Chatroom',
+				hidden = true,
+			})
+			tab = multishell.getTab(tabId)
+		end
+	)
+
+	print('Chat daemon stopped')
+end