init.lua 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. require "core.strict"
  2. local common = require "core.common"
  3. local config = require "core.config"
  4. local style = require "core.style"
  5. local command
  6. local keymap
  7. local RootView
  8. local StatusView
  9. local CommandView
  10. local Doc
  11. local View
  12. local core = {}
  13. local function project_scan_thread()
  14. local function diff_files(a, b)
  15. if #a ~= #b then return true end
  16. for i, v in ipairs(a) do
  17. if b[i].filename ~= v.filename or b[i].type ~= v.type then
  18. return true
  19. end
  20. end
  21. end
  22. local function get_files(path, t)
  23. coroutine.yield()
  24. t = t or {}
  25. local size_limit = config.file_size_limit * 10e5
  26. local all = system.list_dir(path)
  27. local dirs, files = {}, {}
  28. for _, file in ipairs(all) do
  29. if not file:find("^%.") then
  30. local file = path .. _PATHSEP .. file
  31. local info = system.get_file_info(file)
  32. if info and info.size < size_limit then
  33. table.insert(info.type == "dir" and dirs or files, file)
  34. end
  35. end
  36. end
  37. table.sort(dirs)
  38. for _, dir in ipairs(dirs) do
  39. table.insert(t, { filename = dir, type = "dir" })
  40. get_files(dir, t)
  41. end
  42. table.sort(files)
  43. for _, file in ipairs(files) do
  44. table.insert(t, { filename = file, type = "file" })
  45. end
  46. return t
  47. end
  48. while true do
  49. -- get project files and replace previous table if the new table is
  50. -- different
  51. local t = get_files(core.project_dir)
  52. if diff_files(core.project_files, t) then
  53. core.project_files = t
  54. core.redraw = true
  55. end
  56. -- wait for next scan
  57. coroutine.yield(config.project_scan_rate)
  58. end
  59. end
  60. function core.init()
  61. command = require "core.command"
  62. keymap = require "core.keymap"
  63. RootView = require "core.rootview"
  64. View = require "core.view"
  65. StatusView = require "core.statusview"
  66. CommandView = require "core.commandview"
  67. Doc = require "core.doc"
  68. core.frame_start = 0
  69. core.clip_rect_stack = {{ 0,0,0,0 }}
  70. core.log_items = {}
  71. core.docs = {}
  72. core.threads = setmetatable({}, { __mode = "k" })
  73. core.project_files = {}
  74. core.project_dir = "."
  75. local info = _ARGS[2] and system.get_file_info(_ARGS[2])
  76. if info and info.type == "dir" then
  77. core.project_dir = _ARGS[2]:gsub("[\\/]$", "")
  78. end
  79. core.root_view = RootView()
  80. core.command_view = CommandView()
  81. core.status_view = StatusView()
  82. core.root_view.root_node:split("down", core.command_view, true)
  83. core.root_view.root_node.b:split("down", core.status_view, true)
  84. core.active_view = core.root_view.root_node.a.active_view
  85. core.add_thread(project_scan_thread)
  86. command.add_defaults()
  87. local got_plugin_error = not core.load_plugins()
  88. local got_user_error = not core.try(require, "user")
  89. for i = 2, #_ARGS do
  90. local filename = _ARGS[i]
  91. local info = system.get_file_info(filename)
  92. if info and info.type == "file" then
  93. core.root_view:open_doc(core.open_doc(filename))
  94. end
  95. end
  96. if got_plugin_error or got_user_error then
  97. command.perform("core:open-log")
  98. end
  99. end
  100. function core.quit(force)
  101. if force then
  102. os.exit()
  103. end
  104. local dirty_count = 0
  105. local dirty_name
  106. for _, doc in ipairs(core.docs) do
  107. if doc:is_dirty() then
  108. dirty_count = dirty_count + 1
  109. dirty_name = doc:get_name()
  110. end
  111. end
  112. if dirty_count > 0 then
  113. local text
  114. if dirty_count == 1 then
  115. text = string.format("%q has unsaved changes. Quit anyway?", dirty_name)
  116. else
  117. text = string.format("%d docs have unsaved changes. Quit anyway?", dirty_count)
  118. end
  119. local confirm = system.show_confirm_dialog("Unsaved Changes", text)
  120. if not confirm then return end
  121. end
  122. core.quit(true)
  123. end
  124. function core.load_plugins()
  125. local no_errors = true
  126. local files = system.list_dir(_EXEDIR .. "/data/plugins")
  127. for _, filename in ipairs(files) do
  128. local modname = "plugins." .. filename:gsub(".lua$", "")
  129. local ok = core.try(require, modname)
  130. if ok then
  131. core.log_quiet("Loaded plugin %q", modname)
  132. else
  133. no_errors = false
  134. end
  135. end
  136. return no_errors
  137. end
  138. function core.reload_module(name)
  139. local old = package.loaded[name]
  140. package.loaded[name] = nil
  141. local new = require(name)
  142. if type(old) == "table" then
  143. for k, v in pairs(new) do old[k] = v end
  144. package.loaded[name] = old
  145. end
  146. end
  147. function core.add_thread(f, weak_ref)
  148. local key = weak_ref or #core.threads + 1
  149. core.threads[key] = { cr = coroutine.create(f), wake = 0 }
  150. end
  151. function core.push_clip_rect(x, y, w, h)
  152. local x2, y2, w2, h2 = table.unpack(core.clip_rect_stack[#core.clip_rect_stack])
  153. local r, b, r2, b2 = x+w, y+h, x2+w2, y2+h2
  154. x, y = math.max(x, x2), math.max(y, y2)
  155. b, r = math.min(b, b2), math.min(r, r2)
  156. w, h = r-x, b-y
  157. table.insert(core.clip_rect_stack, { x, y, w, h })
  158. renderer.set_clip_rect(x, y, w, h)
  159. end
  160. function core.pop_clip_rect()
  161. table.remove(core.clip_rect_stack)
  162. local x, y, w, h = table.unpack(core.clip_rect_stack[#core.clip_rect_stack])
  163. renderer.set_clip_rect(x, y, w, h)
  164. end
  165. function core.open_doc(filename)
  166. if filename then
  167. -- try to find existing doc for filename
  168. local abs_filename = system.absolute_path(filename)
  169. for _, doc in ipairs(core.docs) do
  170. if doc.filename
  171. and abs_filename == system.absolute_path(doc.filename) then
  172. return doc
  173. end
  174. end
  175. end
  176. -- no existing doc for filename; create new
  177. local doc = Doc(filename)
  178. table.insert(core.docs, doc)
  179. core.log_quiet(filename and "Opened doc %q" or "Opened new doc", filename)
  180. return doc
  181. end
  182. function core.get_views_referencing_doc(doc)
  183. local res = {}
  184. local views = core.root_view.root_node:get_children()
  185. for _, view in ipairs(views) do
  186. if view.doc == doc then table.insert(res, view) end
  187. end
  188. return res
  189. end
  190. local function log(icon, icon_color, fmt, ...)
  191. local text = string.format(fmt, ...):gsub("%s", " ")
  192. if icon then
  193. core.status_view:show_message(icon, icon_color, text)
  194. end
  195. local view = core.active_view and core.active_view:get_name()
  196. local item = { text = text, time = os.time(), view = view }
  197. table.insert(core.log_items, item)
  198. if #core.log_items > config.max_log_items then
  199. table.remove(core.log_items, 1)
  200. end
  201. return item
  202. end
  203. function core.log(...)
  204. return log("i", style.text, ...)
  205. end
  206. function core.log_quiet(...)
  207. return log(nil, nil, ...)
  208. end
  209. function core.error(...)
  210. return log("!", style.accent, ...)
  211. end
  212. function core.try(fn, ...)
  213. local err
  214. local ok, res = xpcall(fn, function(msg)
  215. local item = core.error(msg)
  216. item.info = debug.traceback(nil, 2):gsub("\t", "")
  217. err = msg
  218. end, ...)
  219. if ok then
  220. return true, res
  221. end
  222. return false, err
  223. end
  224. function core.on_event(type, ...)
  225. local did_keymap = false
  226. if type == "textinput" then
  227. core.root_view:on_text_input(...)
  228. elseif type == "keypressed" then
  229. did_keymap = keymap.on_key_pressed(...)
  230. elseif type == "keyreleased" then
  231. keymap.on_key_released(...)
  232. elseif type == "mousemoved" then
  233. core.root_view:on_mouse_moved(...)
  234. elseif type == "mousepressed" then
  235. core.root_view:on_mouse_pressed(...)
  236. elseif type == "mousereleased" then
  237. core.root_view:on_mouse_released(...)
  238. elseif type == "mousewheel" then
  239. core.root_view:on_mouse_wheel(...)
  240. elseif type == "filedropped" then
  241. local mx, my = core.root_view.mouse.x, core.root_view.mouse.y
  242. local ok, doc = core.try(core.open_doc, select(1, ...))
  243. if ok then
  244. core.root_view:on_mouse_pressed("left", mx, my, 1)
  245. core.root_view:open_doc(doc)
  246. end
  247. elseif type == "quit" then
  248. core.quit()
  249. end
  250. return did_keymap
  251. end
  252. function core.step()
  253. -- handle events
  254. local event_count = 0
  255. local did_keymap = false
  256. local mouse_moved = false
  257. local mouse = { x = 0, y = 0, dx = 0, dy = 0 }
  258. for type, a,b,c,d in system.poll_event do
  259. if type == "mousemoved" then
  260. mouse_moved = true
  261. mouse.x, mouse.y = a, b
  262. mouse.dx, mouse.dy = mouse.dx + c, mouse.dy + d
  263. elseif type == "textinput" and did_keymap then
  264. did_keymap = false
  265. else
  266. did_keymap = core.on_event(type, a, b, c, d) or did_keymap
  267. end
  268. event_count = event_count + 1
  269. end
  270. if mouse_moved then
  271. core.on_event("mousemoved", mouse.x, mouse.y, mouse.dx, mouse.dy)
  272. end
  273. local width, height = renderer.get_size()
  274. -- update
  275. core.root_view.size.x, core.root_view.size.y = width, height
  276. core.root_view:update()
  277. if not (event_count > 0 or core.redraw) then
  278. return
  279. end
  280. core.redraw = false
  281. -- close unreferenced docs
  282. for i = #core.docs, 1, -1 do
  283. local doc = core.docs[i]
  284. if #core.get_views_referencing_doc(doc) == 0 then
  285. table.remove(core.docs, i)
  286. core.log_quiet("Closed doc %q", doc:get_name())
  287. end
  288. end
  289. -- update window title
  290. local name = core.active_view:get_name()
  291. if name ~= "---" then
  292. system.set_window_title(name .. " - lite")
  293. else
  294. system.set_window_title("lite")
  295. end
  296. -- draw
  297. renderer.begin_frame()
  298. core.clip_rect_stack[1] = { 0, 0, width, height }
  299. renderer.set_clip_rect(table.unpack(core.clip_rect_stack[1]))
  300. core.root_view:draw()
  301. renderer.end_frame()
  302. end
  303. local run_threads = coroutine.wrap(function()
  304. while true do
  305. local max_time = 1 / config.fps - 0.004
  306. local ran_any_threads = false
  307. for k, thread in pairs(core.threads) do
  308. -- run thread
  309. if thread.wake < system.get_time() then
  310. local _, wait = assert(coroutine.resume(thread.cr))
  311. if coroutine.status(thread.cr) == "dead" then
  312. if type(k) == "number" then
  313. table.remove(core.threads, k)
  314. else
  315. core.threads[k] = nil
  316. end
  317. elseif wait then
  318. thread.wake = system.get_time() + wait
  319. end
  320. ran_any_threads = true
  321. end
  322. -- stop running threads if we're about to hit the end of frame
  323. if system.get_time() - core.frame_start > max_time then
  324. coroutine.yield()
  325. end
  326. end
  327. if not ran_any_threads then coroutine.yield() end
  328. end
  329. end)
  330. function core.run()
  331. while true do
  332. core.frame_start = system.get_time()
  333. core.step()
  334. run_threads()
  335. local elapsed = system.get_time() - core.frame_start
  336. system.sleep(math.max(0, 1 / config.fps - elapsed))
  337. end
  338. end
  339. function core.on_error(err)
  340. -- write error to file
  341. local fp = io.open(_EXEDIR .. "/error.txt", "wb")
  342. fp:write("Error: " .. tostring(err) .. "\n")
  343. fp:write(debug.traceback(nil, 4))
  344. fp:close()
  345. -- save copy of all unsaved documents
  346. for _, doc in ipairs(core.docs) do
  347. if doc:is_dirty() and doc.filename then
  348. doc:save(doc.filename .. "~")
  349. end
  350. end
  351. end
  352. return core