aboutsummaryrefslogtreecommitdiffstats
path: root/alv/copilot
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2020-05-15 11:22:56 +0000
committers-ol <s+removethis@s-ol.nu>2025-03-02 14:24:49 +0000
commitdceb06938c1295d1a4a7eafff20d5007e48dab81 (patch)
tree5984aa544f188d4033fda2b40c91e4a10005f4a8 /alv/copilot
parentadd array and struct constructors (diff)
downloadalive-dceb06938c1295d1a4a7eafff20d5007e48dab81.tar.gz
alive-dceb06938c1295d1a4a7eafff20d5007e48dab81.zip
move copilot logic, make startup scripts lua
Diffstat (limited to 'alv/copilot')
-rw-r--r--alv/copilot/base.moon152
-rw-r--r--alv/copilot/cli.moon34
-rw-r--r--alv/copilot/fltk.moon111
-rw-r--r--alv/copilot/wx.moon139
4 files changed, 436 insertions, 0 deletions
diff --git a/alv/copilot/base.moon b/alv/copilot/base.moon
new file mode 100644
index 0000000..8669d93
--- /dev/null
+++ b/alv/copilot/base.moon
@@ -0,0 +1,152 @@
+----
+-- File watcher and runtime entrypoint.
+--
+-- @classmod Copilot
+lfs = require 'lfs'
+import Scope from require 'alv.scope'
+import Module from require 'alv.module'
+import Error from require 'alv.error'
+import RTNode from require 'alv.rtnode'
+import Constant from require 'alv.result'
+
+parse_args = (args, out={}) ->
+ local key
+ for a in *args
+ if key
+ out[key] = a
+ key = nil
+ else if match = a\match '^%-%-(.*)'
+ if 'boolean' == type out[match]
+ out[match] = true
+ else
+ key = match
+ else
+ table.insert out, a
+ assert not key, "value for option '--#{key}' missing!"
+ out
+
+export COPILOT
+
+class Copilot
+--- static functions
+-- @section static
+
+ --- create a new Copilot.
+ -- @classmethod
+ -- @tparam string file name/path of the alive file to watch and execute
+ new: (file) =>
+ @T = 0
+ @last_modification = 0
+ @last_modules = {}
+ @open file if file
+
+--- members
+-- @section members
+
+ --- current tick
+ -- @tfield number T
+
+ --- change the running script.
+ -- @tparam string file
+ open: (file) =>
+ assert not COPILOT, "another Copilot is already running!"
+ COPILOT = @
+
+ if old = @last_modules.__root
+ old\destroy!
+
+ @last_modules.__root = Module file
+ @active_module = @last_modules.__root
+
+ COPILOT = nil
+
+ --- require a module.
+ -- @tparam string name
+ -- @treturn RTNode root
+ require: (name) =>
+ Error.wrap "loading module '#{name}'", ->
+ ok, lua = pcall require, "alv-lib.#{name}"
+ if ok
+ RTNode result: Constant.wrap lua
+ elseif not lua\match "not found"
+ error lua
+ else
+ assert @modules, "no current eval cycle?"
+ if mod = @modules[name]
+ mod.root\make_ref!
+ else
+ last = @active_module
+ prefix = if b = last.file\match'(.*)/[^/]*$' then b .. '/' else ''
+ mod = @last_modules[name] or Module "#{prefix}#{name}.alv"
+ L\trace "entering module #{mod}"
+ @modules[name] = mod
+ @active_module = mod
+ ok, err = pcall mod\eval
+ L\trace "returning to module #{mod}"
+ @active_module = last
+ if ok
+ mod.root
+ else
+ error err
+
+ --- poll for changes and tick.
+ tick: =>
+ assert not COPILOT, "another Copilot is already running!"
+ return unless @last_modules.__root
+
+ COPILOT = @
+ @T += 1
+
+ @poll!
+
+ root = @last_modules.__root
+ if root and root.root
+ L\set_time 'run'
+ ok, error = Error.try "updating", ->
+ root.root\poll_io!
+ root.root\tick!
+ if not ok
+ L\print error
+
+ COPILOT = nil
+
+ --- poll all loaded modules for changes.
+ --
+ -- Call `eval` if there are any, and write changed and newly added modules
+ -- back to disk.
+ poll: =>
+ dirty = {}
+ for name, mod in pairs @last_modules
+ if mod\poll! > @last_modification
+ table.insert dirty, mod
+
+ return if #dirty == 0
+
+ @last_modification = os.time!
+ L\set_time 'eval'
+ L\print "changes to files: #{table.concat [m.file for m in *dirty], ', '}"
+
+ @modules = { __root: @last_modules.__root }
+ ok, err = Error.try "processing changes", @modules.__root\eval
+
+ if not ok
+ for name, mod in pairs @modules
+ mod\rollback!
+ @modules = nil
+ L\error err
+ return
+
+ for name, mod in pairs @last_modules
+ if not @modules[name]
+ mod\destroy!
+
+ for name, mod in pairs @modules
+ mod\finish!
+
+ @last_modification = os.time!
+ @last_modules, @modules = @modules, nil
+
+{
+ :parse_args
+ :Copilot
+}
diff --git a/alv/copilot/cli.moon b/alv/copilot/cli.moon
new file mode 100644
index 0000000..3686491
--- /dev/null
+++ b/alv/copilot/cli.moon
@@ -0,0 +1,34 @@
+----
+-- CLI Copilot entrypoint.
+--
+-- @classmod CLICopilot
+import Logger, version from require 'alv'
+import parse_args, Copilot from require 'alv.copilot.base'
+import sleep from require 'system'
+
+class ColorLogger extends Logger
+ set_time: (time) =>
+ super time
+ @stream\write switch time
+ when 'eval' then '\27[92m'
+ when 'run' then '\27[0m'
+
+class CLICopilot extends Copilot
+ new: (arg) =>
+ @args = parse_args arg, { nocolor: false }
+ assert @args[1], "no filename given"
+ super @args[1]
+
+ run: =>
+ if @args.nocolor
+ Logger\init @args.log
+ else
+ ColorLogger\init @args.log
+
+ while true
+ @tick!
+ sleep 1 / 1000
+
+{
+ :CLICopilot
+}
diff --git a/alv/copilot/fltk.moon b/alv/copilot/fltk.moon
new file mode 100644
index 0000000..8821358
--- /dev/null
+++ b/alv/copilot/fltk.moon
@@ -0,0 +1,111 @@
+----
+-- fltk4lua Copilot GUI.
+--
+-- @classmod FLTKCopilot
+import Logger, version from require 'alv'
+import parse_args, Copilot from require 'alv.copilot.base'
+import sleep from require 'system'
+fl = require 'fltk4lua'
+
+class FLTKLogger extends Logger
+ new: (level, @eval, @run) =>
+ super level
+
+ set_time: (time) =>
+ @output = switch time
+ when 'eval' then @eval
+ when 'run' then @run
+ else error "invalid time '#{time}'"
+
+ put: (message) =>
+ @output.browser\add message
+ if true or @output.sticky.value
+ @output.browser.bottomline = @output.browser.nitems
+
+class FLTKCopilot extends Copilot
+ new: (arg) =>
+ @args = parse_args arg
+
+ @window = with fl.Window { 400, 220, "alv copilot", xclass: 'alv' }
+ \size_range 400, 220, nil, nil, 20, 20
+
+ @menubar = with fl.Menu_Bar 0, 0, 400, 20
+ \add "&File/About", nil, @\about, nil, fl.MENU_DIVIDER
+ \add "&File/&Open script", '^o', -> @open!
+ \add "&File/&Quit", '^q', -> @window\hide!
+ @pause = \add "Run (^P)", '^p', @\pause, nil, fl.MENU_TOGGLE
+ \add "Help", nil, -> fl.open_uri version.web
+
+ tile = fl.Tile 5, 40, 390, 160
+ @window.resizable = tile
+ resize_box = fl.Box { 5, 60, 390, 120, labeltype: fl.NO_LABEL }
+ tile.resizable = resize_box
+
+ @eval_out = do
+ browser = fl.Browser {
+ 5, 40, 390, 80, "eval",
+ box: fl.GTK_DOWN_BOX, align: fl.ALIGN_TOP_RIGHT
+ }
+ -- sticky = fl.Check_Button 5, 20, 80, 20, 'sticky'
+ -- sticky\set!
+ :browser, :sticky
+
+ @run_out = do
+ browser = fl.Browser {
+ 5, 120, 390, 80, "run",
+ box: fl.GTK_DOWN_BOX, align: fl.ALIGN_BOTTOM_RIGHT
+ }
+ -- sticky = fl.Check_Button 5, 200, 80, 20, 'sticky'
+ -- sticky\set!
+ :browser, :sticky
+
+ tile\end_group!
+ @window\end_group!
+
+ @update_status!
+ super @args[1]
+
+ about: =>
+ fl.alert "alive #{version.tag} fltkCopilot.
+
+visit #{version.web} for more information."
+
+ open: (file) =>
+ file or= fl.file_chooser "Open Script", '*.alv', 'script.alv'
+
+ if file
+ @paused = false
+ super file
+ @update_status!
+
+ pause: =>
+ @paused = not @paused
+ @update_status!
+
+ update_status: =>
+ state = fl.MENU_TOGGLE
+ if not @paused
+ state += fl.MENU_VALUE
+ @menubar\menuitem_setp @pause, 'flags', state
+
+ if @paused
+ "Paused."
+ else
+ "Running."
+
+ run: =>
+ FLTKLogger\init @args.log, @eval_out, @run_out
+ @window\show!
+
+ run = true
+ while run
+ run = if @paused
+ fl.check 1
+ else
+ @tick!
+ sleep 1 / 1000
+ fl.check!
+
+{
+ :FLTKCopilot
+}
diff --git a/alv/copilot/wx.moon b/alv/copilot/wx.moon
new file mode 100644
index 0000000..b740794
--- /dev/null
+++ b/alv/copilot/wx.moon
@@ -0,0 +1,139 @@
+----
+-- wxLua Copilot GUI.
+--
+-- @classmod WXCopilot
+import Logger, version from require 'alv'
+import parse_args, Copilot from require 'alv.copilot.base'
+
+require 'wx'
+import
+ wxID_ABOUT, wxID_OPEN, wxID_EXIT, wxID_ANY, wxVERTICAL,
+ wxFrame, wxMenuBar, wxMenu, wxPanel, wxBoxSizer, wxTextCtrl, wxSplitterWindow
+from wx
+
+STARTSTOP = 100
+
+class WXLogger extends Logger
+ new: (level, @eval_ctrl, @run_ctrl) =>
+ super level
+
+ set_time: (time) =>
+ @ctrl = switch time
+ when 'eval' then @eval_ctrl
+ when 'run' then @run_ctrl
+ else error "invalid time '#{time}'"
+
+ put: (message) =>
+ @ctrl\AppendText message .. '\n'
+
+class WXCopilot extends Copilot
+ new: (arg) =>
+ @args = parse_args arg
+ super @args[1]
+
+ @app = wx.wxGetApp!
+ @app.VendorName = 'alive'
+ @app.AppName = 'alive wxCopilot'
+
+ fileMenu = wxMenu {
+ { wxID_ABOUT, '&About', 'About alive wxCopilot' }
+ { wxID_OPEN, '&Open\tCtrl-O', 'Open Script' }
+ { }
+ { wxID_EXIT, 'E&xit\tCtrl-Q', 'Exit Program' }
+ }
+ runMenu = wxMenu {
+ { STARTSTOP, '&Start/Stop\tCtrl-P', 'Start/Stop Script Execution' }
+ }
+ @menuBar = with wxMenuBar!
+ \Append fileMenu, '&File'
+ \Append runMenu, '&Run'
+
+ @frame = wxFrame wx.NULL, wxID_ANY, @app\GetAppName!
+ @frame\SetMenuBar @menuBar
+ @status = @frame\CreateStatusBar 1
+
+ @update_status!
+
+ splitter = wxSplitterWindow @frame, wx.wxID_ANY
+
+ eval, @eval_ctrl = @mkPanel splitter, 'eval-time messages'
+
+ run, @run_ctrl = @mkPanel splitter, 'run-time messages'
+
+ splitter\SetMinimumPaneSize 60
+ splitter\SplitHorizontally eval, run, 0
+
+ sizer = with wxBoxSizer wx.wxVERTICAL
+ \Add splitter, 1, wx.wxEXPAND + wx.wxALL, 10
+
+ @frame\SetAutoLayout true
+ @frame\SetSizer sizer
+ @frame\Show true
+
+ @frame\Connect wxID_ABOUT, wx.wxEVT_COMMAND_MENU_SELECTED, @\do_about
+ @frame\Connect wxID_OPEN, wx.wxEVT_COMMAND_MENU_SELECTED, @\do_open
+ @frame\Connect wxID_EXIT, wx.wxEVT_COMMAND_MENU_SELECTED, -> @frame\Close!
+ @frame\Connect STARTSTOP, wx.wxEVT_COMMAND_MENU_SELECTED, @\do_startstop
+ @frame\Connect wxID_ANY, wx.wxEVT_IDLE, @\do_idle
+
+ do_about: =>
+ wx.wxMessageBox "alive #{version.tag} wxCopilot.
+
+built using #{wxlua.wxLUA_VERSION_STRING} on #{wx.wxVERSION_STRING}",
+ "About wxCopilot",
+ wx.wxOK + wx.wxICON_INFORMATION,
+ @frame
+
+ do_open: =>
+ dialog = wx.wxFileDialog @frame, 'Change Script', '', '',
+ 'Alive scripts (*.alv)|*.alv|All files (*)|*',
+ wx.wxFD_OPEN + wx.wxFD_FILE_MUST_EXIST
+
+ if dialog\ShowModal! == wx.wxID_OK
+ @paused = false
+ @open dialog\GetPath!
+ @update_status!
+
+ dialog\Destroy!
+
+ do_startstop: (event) =>
+ if @file
+ @paused = not @paused
+ @update_status!
+
+ do_idle: (event) =>
+ if not @paused
+ event\RequestMore true
+ @tick!
+
+ update_status: =>
+ startstop = @menuBar\FindItem STARTSTOP
+ if not @file
+ @status\SetStatusText "No script loaded."
+ startstop\Enable false
+ else
+ startstop\Enable true
+ if @paused
+ @status\SetStatusText "Paused."
+ else
+ @status\SetStatusText "Running."
+
+ mkPanel: (parent, name) =>
+ panel = wxPanel parent, wxID_ANY
+ sizer = wxBoxSizer wxVERTICAL
+ panel\SetSizer sizer
+
+ sizer\Add wx.wxStaticText panel, wx.wxID_ANY, name
+ log = wxTextCtrl panel, wxID_ANY, '', wx.wxDefaultPosition,
+ wx.wxDefaultSize, wx.wxTE_MULTILINE + wx.wxTE_READONLY
+ sizer\Add log, 1, wx.wxEXPAND | wx.wxBOTTOM, 5
+
+ panel, log
+
+ run: =>
+ WXLogger\init @args.log, copilot.eval_ctrl, copilot.run_ctrl
+ @app\MainLoop!
+
+{
+ :WXCopilot
+}