aboutsummaryrefslogtreecommitdiffstats
path: root/main.lua
blob: 83316b8e947254727347d1f901545cc7b742cf0a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
-- Copyright (c) 2024 squibid, see LICENSE file for more info
local mp = require('mp')
local msg = require('mp.msg')
local utils = require('mp.utils')

local proc = require('proc')

-- load the config file
local config = dofile(mp.command_native({"expand-path", "~~/eatit-cfg.lua"}))
if not config or type(config) ~= "table" then
  msg.fatal("no config provided, bailing out")
  return
end

local base_dir = mp.command_native({"expand-path", "~~cache/plugins"})
local packages = {}

-- make sure the base directory exists (*nix only)
if utils.file_info(base_dir) == nil then
  proc.exec({ "mkdir", "-p", base_dir }, {}, function(err, message)
    if err then
      msg.fatal(string.format("failed to create plugin directory: %s", err))
      return
    end
  end)
end

--- regiester a new package spec
---@param spec table package spec from config
---@return table package
local function register_pkg(spec)
  if type(spec) ~= "table" then
    spec = { spec }
  end

  local id = spec[1]
  local package = packages[id]

  if not package then
    package = {
      id = id,
      exists = false,
      setup = false
    }

    packages[id] = package
  end

  package.name = string.sub(package.id, string.find(package.id, "%/") + 1, #package.id)
  package.url = spec.url or ("https://github.com/"..package.id..".git")
  package.branch = spec.branch
  package.files = spec.files
  package.dir = package.files and utils.join_path(base_dir, package.name) or utils.join_path(mp.command_native({ 'expand-path', "~~/scripts" }), package.name)
  package.pin = spec.pin

  package.exists = utils.file_info(package.dir) ~= nil
  -- validate that all files have been installed
  if type(package.files) == "table" then
    for filename, dest in pairs(package.files) do
      if not utils.file_info(utils.join_path(package.dir, filename)) or
        not utils.file_info(utils.join_path(mp.command_native({ "expand-path", dest }), filename)) then
        package.exists = false
        break
      end
    end
  end

  package.on_setup = spec.setup

  return package
end

--- run package setup
---@param package table package
local function setup_package(package)
  if type(package.on_setup) ~= "function" then
    return
  end

  local ok, err = pcall(package.on_setup, package.dir)
  if not ok then
    msg.warn(string.format("error when running setup on '%s': %s", package.id, err))
    return
  end
  package.setup = true
end

--- copy all files according to package spec
---@param package table package
local function copy_files(package)
  --- copy src to dest
  ---@param src string path to src file
  ---@param dest string path to dest file
  local function cp(src, dest)
    local i = io.open(src, 'r')
    if not i then return end
    local o = io.open(dest, 'w')
    if not o then return end
    o:write(i:read('*a'))
    o:close()
    i:close()
  end

  if type(package.files) == "table" then
    for name, loc in pairs(package.files) do
      local path = mp.command_native({'expand-path', loc})
      local dest = utils.join_path(path, name)

      local src = utils.join_path(package.dir, name)
      if not utils.file_info(src) then
        msg.warn(string.format("file %s not found", name))
        return
      end
      local ok, err = pcall(cp, src, dest)
      if not ok then
        msg.warn(string.format("failed to copy %s: %s", name, utils.to_string(err)))
      end
    end
  end
end

local function validate_package()
end

--- download or update package
---@param package table package
---@param cb function callback
local function sync(package, cb)
  if package.exists then
    if package.pin then
      cb()
      return
    end

    --- generic error
    ---@param err any error
    local function log_err(err)
      msg.error(string.format("failed to update %s; reason: %s", package.id, err))
    end

    -- get current head commit hash
    proc.git_rev_parse(package.dir, "HEAD", function(err, before)
      if err then
        log_err(before)
        cb(err)
        return
      end

      -- get the latest commit hash
      proc.git_fetch(package.dir, "origin", package.branch or "HEAD", function(err, message)
        if err then
          log_err(message)
          cb(err)
          return
        end

        -- check the latest and current against eachother
        proc.git_rev_parse(package.dir, "FETCH_HEAD", function(err, after)
          if err then
            log_err(after)
            cb(err)
            return
          elseif before == after then
            msg.info(string.format("skipped %s", package.id))
            cb(err)
            return
          end

          -- switch HEAD to new commit
          proc.git_reset(package.dir, after, function(err, message)
            if err then
              log_err(message)
              return
            end
            setup_package(package)
            copy_files(package)
            msg.info(string.format("updated %s; %s -> %s", package.id, before, after))

            cb(err)
          end)
        end)
      end)
    end)
  else
    -- clone repo since it doesn't exist
    proc.git_clone(package.dir, package.url, package.branch, function(err, message)
      if err then
        msg.error(string.format("failed to install %s; reason: %s", package.id, utils.to_string(message)))
      else
        setup_package(package)
        copy_files(package)
        package.exists = true
        msg.info(string.format("installed %s", package.id))
      end

      cb(err)
    end)
  end
end

--- sync a list of plugins
---@param list table list of packages
---@param cb function callback
local function sync_list(list, cb)
  local progress = 0

  for i in pairs(list) do
    sync(list[i], function()
      progress = progress + 1
      cb()
    end)
  end
end

--- check if package spec should be synced
---@param package table package
---@return boolean
local function should_sync(package)
  if config.sync == "new" or config.sync == nil then
    return not package.exists
  else
    return config.sync == "always"
  end
end

-- register all packages
for i = 1, #config do
  local ok, err = pcall(register_pkg, config[i])
  if not ok then
    msg.warn(string.format("%s: %s", err, config[i].as))
  end
end

-- check for package updates
local targets = {}
for i in pairs(packages) do
  if should_sync(packages[i]) then
    targets[#targets + 1] = packages[i]
  end
end

sync_list(targets, function() end)

-- register script message for keybinding
mp.register_script_message("eatit-sync", function(name, value)
  sync_list(packages, function() end)
end)