From 26564cbcc13e4478bac82b928c2f8c35ee5d8bd9 Mon Sep 17 00:00:00 2001
From: "kepler155c@gmail.com" <kepler155c@gmail.com>
Date: Sat, 22 Dec 2018 22:11:16 -0500
Subject: [PATCH] custom help files + redo System UI

---
 sys/apis/config.lua           |   1 +
 sys/apis/turtle/home.lua      |  42 +++++
 sys/apis/ui.lua               |  10 +-
 sys/apps/System.lua           | 339 +++++-----------------------------
 sys/apps/system/aliases.lua   |  66 +++++++
 sys/apps/system/label.lua     |  49 +++++
 sys/apps/system/network.lua   |  56 ++++++
 sys/apps/system/password.lua  |  53 ++++++
 sys/apps/system/path.lua      |  50 +++++
 sys/apps/system/settings.lua  |  47 +++++
 sys/apps/system/turtle.lua    |  62 +++++++
 sys/extensions/5.network.lua  |   8 +-
 sys/extensions/6.packages.lua |  13 +-
 sys/help/Opus Applications    |  30 +++
 sys/help/Wireless Networking  |   7 +
 15 files changed, 541 insertions(+), 292 deletions(-)
 create mode 100644 sys/apis/turtle/home.lua
 create mode 100644 sys/apps/system/aliases.lua
 create mode 100644 sys/apps/system/label.lua
 create mode 100644 sys/apps/system/network.lua
 create mode 100644 sys/apps/system/password.lua
 create mode 100644 sys/apps/system/path.lua
 create mode 100644 sys/apps/system/settings.lua
 create mode 100644 sys/apps/system/turtle.lua
 create mode 100644 sys/help/Opus Applications
 create mode 100644 sys/help/Wireless Networking

diff --git a/sys/apis/config.lua b/sys/apis/config.lua
index 13ab441..35f7310 100644
--- a/sys/apis/config.lua
+++ b/sys/apis/config.lua
@@ -7,6 +7,7 @@ local Config = { }
 
 function Config.load(fname, data)
 	local filename = 'usr/config/' .. fname
+	data = data or { }
 
 	if not fs.exists('usr/config') then
 		fs.makeDir('usr/config')
diff --git a/sys/apis/turtle/home.lua b/sys/apis/turtle/home.lua
new file mode 100644
index 0000000..29a1cbc
--- /dev/null
+++ b/sys/apis/turtle/home.lua
@@ -0,0 +1,42 @@
+local Config = require('config')
+local GPS    = require('gps')
+
+local turtle = _G.turtle
+
+local Home = { }
+
+function Home.go()
+  local config = { }
+  Config.load('gps', config)
+
+  if config.home then
+    if turtle.enableGPS() then
+      return turtle.pathfind(config.home)
+    end
+  end
+end
+
+function Home.set()
+  local config = { }
+  Config.load('gps', config)
+
+  local pt = GPS.getPoint()
+  if pt then
+    local originalHeading = turtle.point.heading
+    local heading = GPS.getHeading()
+    if heading then
+      local turns = (turtle.point.heading - originalHeading) % 4
+      pt.heading = (heading - turns) % 4
+      config.home = pt
+      Config.update('gps', config)
+
+      pt = GPS.getPoint()
+      pt.heading = heading
+      turtle.setPoint(pt, true)
+      turtle._goto(config.home)
+      return config.home
+    end
+  end
+end
+
+return Home
diff --git a/sys/apis/ui.lua b/sys/apis/ui.lua
index dffb370..c9bcabc 100644
--- a/sys/apis/ui.lua
+++ b/sys/apis/ui.lua
@@ -2250,6 +2250,13 @@ function UI.Tabs:add(children)
 	end
 end
 
+function UI.Tabs:selectTab(tab)
+	local menuItem = Util.find(self.tabBar:getFocusables(), 'tabUid', tab.uid)
+	if menuItem then
+		self.tabBar:emit({ type = 'tab_select', button = { uid = menuItem.uid } })
+	end
+end
+
 function UI.Tabs:enable()
 	self.enabled = true
 	self.tabBar:enable()
@@ -2999,6 +3006,7 @@ function UI.Chooser:eventHandler(event)
 		if event.key == 'right' or event.key == 'space' then
 			local _,k = Util.find(self.choices, 'value', self.value)
 			local choice
+			if not k then k = 1 end
 			if k and k < #self.choices then
 				choice = self.choices[k+1]
 			else
@@ -3021,7 +3029,7 @@ function UI.Chooser:eventHandler(event)
 			self:draw()
 			return true
 		end
-	elseif event.type == 'mouse_click' then
+	elseif event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
 		if event.x == 1 then
 			self:emit({ type = 'key', key = 'left' })
 			return true
diff --git a/sys/apps/System.lua b/sys/apps/System.lua
index ce2a2b1..ed742ef 100644
--- a/sys/apps/System.lua
+++ b/sys/apps/System.lua
@@ -1,16 +1,9 @@
-_G.requireInjector(_ENV)
+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 os         = _G.os
-local settings   = _G.settings
-local shell      = _ENV.shell
-local turtle     = _G.turtle
+local fs     = _G.fs
+local shell  = _ENV.shell
 
 UI:configure('System', ...)
 
@@ -23,116 +16,18 @@ Config.load('shell', env)
 
 local systemPage = UI.Page {
 	tabs = UI.Tabs {
-		pathTab = UI.Window {
-			tabTitle = 'Path',
-			entry = UI.TextEntry {
-				x = 2, y = 2, ex = -2,
-				limit = 256,
-				value = shell.path(),
-				shadowText = 'enter system path',
-				accelerators = {
-					enter = 'update_path',
-				},
-			},
+		settings = UI.Window {
+			tabTitle = 'Category',
 			grid = UI.Grid {
-				y = 4,
-				disableHeader = true,
-				columns = { { key = 'value' } },
+				y = 2,
+				columns = {
+					{ heading = 'Name',        key = 'name'        },
+					{ heading = 'Description', key = 'description' },
+				},
+				sortColumn = 'name',
 				autospace = true,
 			},
 		},
-
-		aliasTab = UI.Window {
-			tabTitle = 'Alias',
-			alias = UI.TextEntry {
-				x = 2, y = 2, ex = -2,
-				limit = 32,
-				shadowText = 'Alias',
-			},
-			path = UI.TextEntry {
-				y = 3, x = 2, ex = -2,
-				limit = 256,
-				shadowText = 'Program path',
-				accelerators = {
-					enter = 'new_alias',
-				},
-			},
-			grid = UI.Grid {
-				y = 5,
-				sortColumn = 'alias',
-				columns = {
-					{ heading = 'Alias',   key = 'alias' },
-					{ heading = 'Program', key = 'path'  },
-				},
-				accelerators = {
-					delete = 'delete_alias',
-				},
-			},
-		},
-
-		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,
-				inactive = true,
-				value = 'Add a password to enable other computers to connect to this one.',
-			}
-		},
-
-		infoTab = UI.Window {
-			tabTitle = 'Info',
-			labelText = UI.Text {
-				x = 3, y = 2,
-				value = 'Label'
-			},
-			label = UI.TextEntry {
-				x = 9, y = 2, ex = -4,
-				limit = 32,
-				value = os.getComputerLabel(),
-				accelerators = {
-					enter = 'update_label',
-				},
-			},
-			grid = UI.ScrollingGrid {
-				y = 3,
-				values = {
-					{ name = '',  value = ''                  },
-					{ name = 'CC version',  value = Util.getVersion()                  },
-					{ name = 'Lua version', value = _VERSION                           },
-					{ name = 'MC version',  value = Util.getMinecraftVersion()         },
-					{ name = 'Disk free',   value = Util.toBytes(fs.getFreeSpace('/')) },
-					{ name = 'Computer ID', value = tostring(os.getComputerID())       },
-					{ name = 'Day',         value = tostring(os.day())                 },
-				},
-				inactive = true,
-				columns = {
-					{ key = 'name',  width = 12 },
-					{ key = 'value' },
-				},
-			},
-		},
 	},
 	notification = UI.Notification(),
 	accelerators = {
@@ -140,177 +35,15 @@ local systemPage = UI.Page {
 	},
 }
 
-if turtle then
-	pcall(function()
-		local Home = require('turtle.home')
--- TODO: dont rely on turtle.home
-		local values = { }
-		Config.load('gps', values.home and { values.home } or { })
-
-		systemPage.tabs:add({
-			gpsTab = UI.Window {
-				tabTitle = 'GPS',
-				labelText = UI.Text {
-					x = 3, y = 2,
-					value = 'On restart, return to this location'
-				},
-				grid = UI.Grid {
-					x = 3, ex = -3, y = 4,
-					height = 2,
-					values = values,
-					inactive = true,
-					columns = {
-						{ heading = 'x', key = 'x' },
-						{ heading = 'y', key = 'y' },
-						{ heading = 'z', key = 'z' },
-					},
-				},
-				button1 = UI.Button {
-					x = 3, y = 7,
-					text = 'Set home',
-					event = 'gps_set',
-				},
-				button2 = UI.Button {
-					ex = -3, y = 7, width = 7,
-					text = 'Clear',
-					event = 'gps_clear',
-				},
-			},
-		})
-		function systemPage.tabs.gpsTab:eventHandler(event)
-			if event.type == 'gps_set' then
-				systemPage.notification:info('Determining location', 10)
-				systemPage:sync()
-				if Home.set() then
-					Config.load('gps', values)
-					self.grid:setValues(values.home and { values.home } or { })
-					self.grid:draw()
-					systemPage.notification:success('Location set')
-				else
-					systemPage.notification:error('Unable to determine location')
-				end
-				return true
-			elseif event.type == 'gps_clear' then
-				fs.delete('usr/config/gps')
-				self.grid:setValues({ })
-				self.grid:draw()
-				return true
-			end
+function systemPage.tabs.settings:eventHandler(event)
+	if event.type == 'grid_select' then
+		local tab = event.selected.tab
+		if not systemPage.tabs[tab.tabTitle] then
+			systemPage.tabs:add({ [ tab.tabTitle ] = tab })
+			tab:disable()
 		end
-	end)
-end
-
-if settings then
-	local values = { }
-	for _,v in pairs(settings.getNames()) do
-		local value = settings.get(v)
-		if not value then
-			value = false
-		end
-		table.insert(values, {
-			name = v,
-			value = value,
-		})
-	end
-
-	systemPage.tabs:add({
-		settingsTab = UI.Window {
-			tabTitle = 'Settings',
-			grid = UI.Grid {
-				y = 1,
-				values = values,
-				autospace = true,
-				sortColumn = 'name',
-				columns = {
-					{ heading = 'Setting',   key = 'name' },
-					{ heading = 'Value', key = 'value'  },
-				},
-			},
-		}
-	})
-	function systemPage.tabs.settingsTab:eventHandler(event)
-		if event.type == 'grid_select' then
-			if not event.selected.value or type(event.selected.value) == 'boolean' then
-				event.selected.value = not event.selected.value
-			end
-			settings.set(event.selected.name, event.selected.value)
-			settings.save('.settings')
-			self.grid:draw()
-			return true
-		end
-	end
-end
-
-function systemPage.tabs.pathTab.grid:draw()
-	self.values = { }
-	for _,v in ipairs(Util.split(env.path, '(.-):')) do
-		table.insert(self.values, { value = v })
-	end
-	self:update()
-	UI.Grid.draw(self)
-end
-
-function systemPage.tabs.pathTab:eventHandler(event)
-	if event.type == 'update_path' then
-		env.path = self.entry.value
-		self.grid:setIndex(self.grid:getIndex())
-		self.grid:draw()
-		Config.update('shell', env)
-		systemPage.notification:success('reboot to take effect')
-		return true
-	end
-end
-
-function systemPage.tabs.aliasTab.grid:draw()
-	self.values = { }
-	for k,v in pairs(env.aliases) do
-		table.insert(self.values, { alias = k, path = v })
-	end
-	self:update()
-	UI.Grid.draw(self)
-end
-
-function systemPage.tabs.aliasTab:eventHandler(event)
-	if event.type == 'delete_alias' then
-		env.aliases[self.grid:getSelected().alias] = nil
-		self.grid:setIndex(self.grid:getIndex())
-		self.grid:draw()
-		Config.update('shell', env)
-		systemPage.notification:success('reboot to take effect')
-		return true
-
-	elseif event.type == 'new_alias' then
-		env.aliases[self.alias.value] = self.path.value
-		self.alias:reset()
-		self.path:reset()
-		self:draw()
-		self:setFocus(self.alias)
-		Config.update('shell', env)
-		systemPage.notification:success('reboot to take effect')
-		return true
-	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)
-		systemPage.notification:success('Label updated')
+		systemPage.tabs:selectTab(tab)
+		self.parent:draw()
 		return true
 	end
 end
@@ -318,13 +51,43 @@ end
 function systemPage:eventHandler(event)
 	if event.type == 'quit' then
 		UI:exitPullEvents()
+
+	elseif event.type == 'success_message' then
+		self.notification:success(event.message)
+
+	elseif event.type == 'info_message' then
+		self.notification:info(event.message)
+
+	elseif event.type == 'error_message' then
+		self.notification:error(event.message)
+
 	elseif event.type == 'tab_activate' then
 		event.activated:focusFirst()
+
 	else
 		return UI.Page.eventHandler(self, event)
 	end
 	return true
 end
 
+local function loadDirectory(dir)
+	local plugins = { }
+	for _, file in pairs(fs.list(dir)) do
+		local s, m = Util.run(_ENV, fs.combine(dir, file))
+		if not s and m then
+			_G.printError('Error loading: ' .. file)
+			error(m or 'Unknown error')
+		elseif s and m then
+			table.insert(plugins, { tab = m, name = m.tabTitle, description = m.description })
+		end
+	end
+	return plugins
+end
+
+local programDir = fs.getDir(shell.getRunningProgram())
+local plugins = loadDirectory(fs.combine(programDir, 'system'), { })
+
+systemPage.tabs.settings.grid:setValues(plugins)
+
 UI:setPage(systemPage)
 UI:pullEvents()
diff --git a/sys/apps/system/aliases.lua b/sys/apps/system/aliases.lua
new file mode 100644
index 0000000..cdb38ca
--- /dev/null
+++ b/sys/apps/system/aliases.lua
@@ -0,0 +1,66 @@
+local Config = require('config')
+local UI     = require('ui')
+
+local aliasTab = UI.Window {
+	tabTitle = 'Aliases',
+	description = 'Shell aliases',
+	alias = UI.TextEntry {
+		x = 2, y = 2, ex = -2,
+		limit = 32,
+		shadowText = 'Alias',
+	},
+	path = UI.TextEntry {
+		y = 3, x = 2, ex = -2,
+		limit = 256,
+		shadowText = 'Program path',
+		accelerators = {
+			enter = 'new_alias',
+		},
+	},
+	grid = UI.Grid {
+		y = 5,
+		sortColumn = 'alias',
+		columns = {
+			{ heading = 'Alias',   key = 'alias' },
+			{ heading = 'Program', key = 'path'  },
+		},
+		accelerators = {
+			delete = 'delete_alias',
+		},
+	},
+}
+
+function aliasTab.grid:draw()
+	self.values = { }
+	local env = Config.load('shell')
+	for k,v in pairs(env.aliases) do
+		table.insert(self.values, { alias = k, path = v })
+	end
+	self:update()
+	UI.Grid.draw(self)
+end
+
+function aliasTab:eventHandler(event)
+	if event.type == 'delete_alias' then
+		local env = Config.load('shell')
+		env.aliases[self.grid:getSelected().alias] = nil
+		self.grid:setIndex(self.grid:getIndex())
+		self.grid:draw()
+		Config.update('shell', env)
+		self:emit({ type = 'success_message', message = 'reboot to take effect' })
+		return true
+
+	elseif event.type == 'new_alias' then
+		local env = Config.load('shell')
+		env.aliases[self.alias.value] = self.path.value
+		self.alias:reset()
+		self.path:reset()
+		self:draw()
+		self:setFocus(self.alias)
+		Config.update('shell', env)
+		self:emit({ type = 'success_message', message = 'reboot to take effect' })
+		return true
+	end
+end
+
+return aliasTab
diff --git a/sys/apps/system/label.lua b/sys/apps/system/label.lua
new file mode 100644
index 0000000..0d03425
--- /dev/null
+++ b/sys/apps/system/label.lua
@@ -0,0 +1,49 @@
+local UI   = require('ui')
+local Util = require('util')
+
+local fs   = _G.fs
+local os   = _G.os
+
+local labelTab = UI.Window {
+	tabTitle = 'Label',
+	description = 'Set the computer label',
+	labelText = UI.Text {
+		x = 3, y = 2,
+		value = 'Label'
+	},
+	label = UI.TextEntry {
+		x = 9, y = 2, ex = -4,
+		limit = 32,
+		value = os.getComputerLabel(),
+		accelerators = {
+			enter = 'update_label',
+		},
+	},
+	grid = UI.ScrollingGrid {
+		y = 3,
+		values = {
+			{ name = '',  value = ''                  },
+			{ name = 'CC version',  value = Util.getVersion()                  },
+			{ name = 'Lua version', value = _VERSION                           },
+			{ name = 'MC version',  value = Util.getMinecraftVersion()         },
+			{ name = 'Disk free',   value = Util.toBytes(fs.getFreeSpace('/')) },
+			{ name = 'Computer ID', value = tostring(os.getComputerID())       },
+			{ name = 'Day',         value = tostring(os.day())                 },
+		},
+		inactive = true,
+		columns = {
+			{ key = 'name',  width = 12 },
+			{ key = 'value' },
+		},
+	},
+}
+
+function labelTab:eventHandler(event)
+	if event.type == 'update_label' then
+		os.setComputerLabel(self.label.value)
+		self:emit({ type = 'success_message', message = 'Label updated' })
+		return true
+	end
+end
+
+return labelTab
diff --git a/sys/apps/system/network.lua b/sys/apps/system/network.lua
new file mode 100644
index 0000000..0b9737d
--- /dev/null
+++ b/sys/apps/system/network.lua
@@ -0,0 +1,56 @@
+local Config = require('config')
+local UI     = require('ui')
+
+local device = _G.device
+
+local tab = UI.Window {
+	tabTitle = 'Network',
+	description = 'Networking options',
+	form = UI.Form {
+		x = 2,
+		manualControls = true,
+		modem = UI.Chooser {
+			formLabel = 'Modem', formKey = 'modem',
+			nochoice = 'auto',
+		},
+		update = UI.Button {
+			x = 9, y = 4,
+			text = 'Update', event = 'form_complete',
+		},
+	},
+}
+
+function tab:enable()
+	local width = 7
+	local choices = {
+		{ name = 'auto',    value = 'auto' },
+		{ name = 'disable', value = 'none' },
+	}
+
+	for k,v in pairs(device) do
+		if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then
+			table.insert(choices, { name = k, value = v.name })
+			width = math.max(width, #k)
+		end
+	end
+
+	self.form.modem.choices = choices
+	self.form.modem.width = width + 4
+
+	local config = Config.load('os')
+	self.form.modem.value = config.wirelessModem or 'auto'
+
+	UI.Window.enable(self)
+end
+
+function tab:eventHandler(event)
+	if event.type == 'form_complete' then
+		local config = Config.load('os')
+		config.wirelessModem = self.form.modem.value
+		Config.update('os', config)
+		self:emit({ type = 'success_message', message = 'reboot to take effect' })
+		return true
+	end
+end
+
+return tab
diff --git a/sys/apps/system/password.lua b/sys/apps/system/password.lua
new file mode 100644
index 0000000..23524fd
--- /dev/null
+++ b/sys/apps/system/password.lua
@@ -0,0 +1,53 @@
+local Security = require('security')
+local SHA1     = require('sha1')
+local UI       = require('ui')
+
+local passwordTab = UI.Window {
+	tabTitle = 'Password',
+	description = 'Wireless network 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,
+		inactive = true,
+		value = 'Add a password to enable other computers to connect to this one.',
+	}
+}
+function passwordTab:eventHandler(event)
+	if event.type == 'update_password' then
+		if #self.newPass.value == 0 then
+			self:emit({ type = 'error_message', message = 'Invalid password' })
+
+		elseif Security.getPassword() and not Security.verifyPassword(SHA1.sha1(self.oldPass.value)) then
+			self:emit({ type = 'error_message', message = 'Passwords do not match' })
+
+		else
+			Security.updatePassword(SHA1.sha1(self.newPass.value))
+			self.oldPass.inactive = false
+			self:emit({ type = 'success_message', message = 'Password updated' })
+		end
+		return true
+	end
+end
+
+return passwordTab
diff --git a/sys/apps/system/path.lua b/sys/apps/system/path.lua
new file mode 100644
index 0000000..d55211a
--- /dev/null
+++ b/sys/apps/system/path.lua
@@ -0,0 +1,50 @@
+local Config = require('config')
+local UI     = require('ui')
+local Util   = require('util')
+
+local shell  = _ENV.shell
+
+local pathTab = UI.Window {
+	tabTitle = 'Path',
+	description = 'Set the shell path',
+	tabClose = true,
+	entry = UI.TextEntry {
+		x = 2, y = 2, ex = -2,
+		limit = 256,
+		value = shell.path(),
+		shadowText = 'enter system path',
+		accelerators = {
+			enter = 'update_path',
+		},
+	},
+	grid = UI.Grid {
+		y = 4,
+		disableHeader = true,
+		columns = { { key = 'value' } },
+		autospace = true,
+	},
+}
+
+function pathTab.grid:draw()
+	self.values = { }
+	local env = Config.load('shell')
+	for _,v in ipairs(Util.split(env.path, '(.-):')) do
+		table.insert(self.values, { value = v })
+	end
+	self:update()
+	UI.Grid.draw(self)
+end
+
+function pathTab:eventHandler(event)
+	if event.type == 'update_path' then
+		local env = Config.load('shell')
+		env.path = self.entry.value
+		self.grid:setIndex(self.grid:getIndex())
+		self.grid:draw()
+		Config.update('shell', env)
+		self:emit({ type = 'success_message', message = 'reboot to take effect' })
+		return true
+	end
+end
+
+return pathTab
diff --git a/sys/apps/system/settings.lua b/sys/apps/system/settings.lua
new file mode 100644
index 0000000..2722dab
--- /dev/null
+++ b/sys/apps/system/settings.lua
@@ -0,0 +1,47 @@
+local UI = require('ui')
+
+local settings = _G.settings
+
+if settings then
+
+	local values = { }
+	for _,v in pairs(settings.getNames()) do
+		local value = settings.get(v)
+		if not value then
+			value = false
+		end
+		table.insert(values, {
+			name = v,
+			value = value,
+		})
+	end
+
+	local settingsTab = UI.Window {
+		tabTitle = 'Settings',
+		description = 'Computercraft configurable settings',
+		grid = UI.Grid {
+			y = 1,
+			values = values,
+			autospace = true,
+			sortColumn = 'name',
+			columns = {
+				{ heading = 'Setting',   key = 'name' },
+				{ heading = 'Value', key = 'value'  },
+			},
+		},
+	}
+
+	function settingsTab:eventHandler(event)
+		if event.type == 'grid_select' then
+			if not event.selected.value or type(event.selected.value) == 'boolean' then
+				event.selected.value = not event.selected.value
+			end
+			settings.set(event.selected.name, event.selected.value)
+			settings.save('.settings')
+			self.grid:draw()
+			return true
+		end
+	end
+
+	return settingsTab
+end
diff --git a/sys/apps/system/turtle.lua b/sys/apps/system/turtle.lua
new file mode 100644
index 0000000..18150de
--- /dev/null
+++ b/sys/apps/system/turtle.lua
@@ -0,0 +1,62 @@
+local Config = require('config')
+local UI     = require('ui')
+
+local fs     = _G.fs
+local turtle = _G.turtle
+
+if turtle then
+	local Home = require('turtle.home')
+	local values = { }
+	Config.load('gps', values.home and { values.home } or { })
+
+	local gpsTab = UI.Window {
+		tabTitle = 'GPS',
+		labelText = UI.Text {
+			x = 3, y = 2,
+			value = 'On restart, return to this location'
+		},
+		grid = UI.Grid {
+			x = 3, ex = -3, y = 4,
+			height = 2,
+			values = values,
+			inactive = true,
+			columns = {
+				{ heading = 'x', key = 'x' },
+				{ heading = 'y', key = 'y' },
+				{ heading = 'z', key = 'z' },
+			},
+		},
+		button1 = UI.Button {
+			x = 3, y = 7,
+			text = 'Set home',
+			event = 'gps_set',
+		},
+		button2 = UI.Button {
+			ex = -3, y = 7, width = 7,
+			text = 'Clear',
+			event = 'gps_clear',
+		},
+	}
+	function gpsTab:eventHandler(event)
+		if event.type == 'gps_set' then
+			self:emit({ type = 'info_message', message = 'Determining location' })
+			self:sync()
+			if Home.set() then
+				Config.load('gps', values)
+				self.grid:setValues(values.home and { values.home } or { })
+				self.grid:draw()
+				self:emit({ type = 'success_message', message = 'Location set' })
+			else
+				self:emit({ type = 'error_message', message = 'Unable to determine location' })
+			end
+			return true
+		elseif event.type == 'gps_clear' then
+			fs.delete('usr/config/gps')
+			self.grid:setValues({ })
+			self.grid:draw()
+			return true
+		end
+	end
+
+	return gpsTab
+end
diff --git a/sys/extensions/5.network.lua b/sys/extensions/5.network.lua
index 3963ae2..417126e 100644
--- a/sys/extensions/5.network.lua
+++ b/sys/extensions/5.network.lua
@@ -18,8 +18,12 @@ end
 
 local function setModem(dev)
 	if not device.wireless_modem and dev.isWireless() then
-		local config = Config.load('os', { })
-		if not config.wirelessModem or dev.name == config.wirelessModem then
+		local config = Config.load('os')
+
+		if not config.wirelessModem or
+			config.wirelessModem == 'auto' or
+			dev.name == config.wirelessModem then
+
 			device.wireless_modem = dev
 			os.queueEvent('device_attach', 'wireless_modem')
 			return dev
diff --git a/sys/extensions/6.packages.lua b/sys/extensions/6.packages.lua
index cc68ddd..876e2d6 100644
--- a/sys/extensions/6.packages.lua
+++ b/sys/extensions/6.packages.lua
@@ -3,8 +3,9 @@ _G.requireInjector(_ENV)
 local Packages = require('packages')
 local Util     = require('util')
 
+local fs    = _G.fs
+local help  = _G.help
 local shell = _ENV.shell
-local fs = _G.fs
 
 local appPaths = Util.split(shell.path(), '(.-);')
 local luaPaths = Util.split(_G.LUA_PATH, '(.-);')
@@ -26,6 +27,9 @@ end
 -- dependency graph
 -- https://github.com/mpeterv/depgraph/blob/master/src/depgraph/init.lua
 
+local helpPaths = Util.split(help.path(), '(.-):')
+table.insert(helpPaths, '/sys/help')
+
 for name in pairs(Packages:installed()) do
 	local packageDir = fs.combine('packages', name)
 	if fs.exists(fs.combine(packageDir, '.install')) then
@@ -40,7 +44,14 @@ for name in pairs(Packages:installed()) do
 	if fs.exists(apiPath) then
 		addPath(luaPaths, apiPath)
 	end
+
+	local helpPath = fs.combine(fs.combine('packages', name), 'help')
+	if fs.exists(helpPath) then
+		table.insert(helpPaths, helpPath)
+	end
 end
 
+help.setPath(table.concat(helpPaths, ':'))
+
 shell.setPath(table.concat(appPaths, ':'))
 _G.LUA_PATH = table.concat(luaPaths, ';')
diff --git a/sys/help/Opus Applications b/sys/help/Opus Applications
new file mode 100644
index 0000000..a7747a3
--- /dev/null
+++ b/sys/help/Opus Applications	
@@ -0,0 +1,30 @@
+Opus applications are grouped into packages with a common theme.
+
+To install a package, use either the System -> Packages program or the package command line program.
+
+Shell usage:
+
+> package list
+> package install <name>
+> package update <name>
+> package uninstall <name>
+
+Package definitions are located in usr/apps/packages. This file can be modified to add custom packages.
+
+Current stable packages
+=======================
+
+* core
+Programming and miscellaneous applications. Also contains drivers needed for other packages.
+
+* builder
+A program for creating structures from schematic files using a turtle (requires core).
+
+* farms
+Various programs for farming resources (wood, crops, animals).
+
+* milo
+An A/E like storage implementation (requires core).
+
+* miners
+Mining programs.
diff --git a/sys/help/Wireless Networking b/sys/help/Wireless Networking
new file mode 100644
index 0000000..9411058
--- /dev/null
+++ b/sys/help/Wireless Networking	
@@ -0,0 +1,7 @@
+To establish one-way trust between two computers with modems, run the following in a shell prompt:
+
+On the target computer, run:
+> password
+
+On the source computer, run:
+> trust <target computer ID>