diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 954d5a6a..f4f16217 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,12 +12,13 @@ on:
 jobs:
   lint:
     runs-on: ubuntu-latest
+    name: Stylua Check
     steps:
       - uses: actions/checkout@v3
       - name: Stylua
-        uses: JohnnyMorganz/stylua-action@v1.1.2
+        uses: JohnnyMorganz/stylua-action@v3
         with:
-          token: ${{ secrets.GITHUB_TOKEN }}
+          version: latest
           args: --check .
 
   docs:
@@ -36,3 +37,32 @@ jobs:
           commit_user_name: "github-actions[bot]"
           commit_user_email: "github-actions[bot]@users.noreply.github.com"
           commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
+
+  test:
+    name: Run Test
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest]
+    steps:
+      - uses: actions/checkout@v3
+      - uses: rhysd/action-setup-vim@v1
+        id: vim
+        with:
+          neovim: true
+          version: nightly
+
+      - name: luajit
+        uses: leafo/gh-actions-lua@v10
+        with:
+          luaVersion: "luajit-2.1.0-beta3"
+
+      - name: luarocks
+        uses: leafo/gh-actions-luarocks@v4
+
+      - name: run test
+        shell: bash
+        run: |
+          luarocks install luacheck
+          luarocks install vusted
+          vusted ./test
diff --git a/doc/dashboard.txt b/doc/dashboard.txt
index 93378c84..e2352345 100644
--- a/doc/dashboard.txt
+++ b/doc/dashboard.txt
@@ -1,4 +1,4 @@
-*dashboard.txt*             For Nvim 0.8.0            Last change: 2023 May 03
+*dashboard.txt*            For Nvim 0.8.0            Last change: 2023 June 08
 
 ==============================================================================
 Table of Contents                                *dashboard-table-of-contents*
@@ -19,7 +19,6 @@ Fancy and Blazing Fast start screen plugin of neovim  --------------------------
 ==============================================================================
 1. Feature                                                 *dashboard-feature*
 
-
 - Low memory usage. dashboard does not store the all user configs in memory like header etc these string will take some memory. now it will be clean after you open a file. you can still use dashboard command to open a new one , then dashboard will read the config from cache.
 - Blazing fast
 
@@ -27,7 +26,6 @@ Fancy and Blazing Fast start screen plugin of neovim  --------------------------
 ==============================================================================
 2. Install                                                 *dashboard-install*
 
-
 - Lazy.nvim
 
 >lua
@@ -43,7 +41,6 @@ Fancy and Blazing Fast start screen plugin of neovim  --------------------------
     }
 <
 
-
 - Packer
 
 >lua
@@ -152,7 +149,6 @@ instead
 
 COMMANDS ~
 
-
 - `Dashboard` open dashboard
 - `DbProjectDelete count` delete project in cache works for hyper theme. count is number
 
@@ -248,7 +244,6 @@ Doom ~
 
 CHANGED ~
 
-
 - Removed Session as a start screen plugin speed is first.if you want use session you can take a
     look at glepnir/dbsession.nvim <https://github.com/glepnir/dbsession.nvim>
 - Removed Ueberzug script, as the Ueberzug author has deleted the repository.
@@ -256,7 +251,6 @@ CHANGED ~
 
 TODO ~
 
-
 - I will write a plugin to implement some popular terminal evaluators image protocol then I think
     can make it work with dashboard
 
diff --git a/lua/dashboard/async.lua b/lua/dashboard/async.lua
new file mode 100644
index 00000000..031f122c
--- /dev/null
+++ b/lua/dashboard/async.lua
@@ -0,0 +1,104 @@
+local uv = vim.loop
+
+local function async(fn)
+  local co = coroutine.create(fn)
+  return function(...)
+    local status, result = coroutine.resume(co, ...)
+    if not status then
+      error(result)
+    end
+    return result
+  end
+end
+
+local function await(promise)
+  if type(promise) ~= 'function' then
+    return promise
+  end
+  local co = assert(coroutine.running())
+  promise(function(...)
+    coroutine.resume(co, ...)
+  end)
+  return (coroutine.yield())
+end
+
+local function fs_module(filename)
+  local fs = {}
+  local task = {}
+
+  function fs:open(flag)
+    task[#task + 1] = function(callback)
+      uv.fs_open(filename, flag, tonumber('644', 8), function(err, fd)
+        assert(not err)
+        self.fd = fd
+        callback(fd)
+      end)
+    end
+    return self
+  end
+
+  function fs:size()
+    task[#task + 1] = function(callback)
+      uv.fs_fstat(self.fd, function(err, stat)
+        assert(not err)
+        self.size = stat.size
+        callback(stat.size)
+      end)
+    end
+    return self
+  end
+
+  function fs:read()
+    task[#task + 1] = function(callback)
+      uv.fs_read(self.fd, self.size, function(err, data)
+        assert(not err)
+        assert(uv.fs_close(self.fd))
+        callback(data)
+      end)
+    end
+    return self
+  end
+
+  function fs:write(content)
+    task[#task + 1] = function()
+      uv.fs_write(self.fd, content, function(err, bytes)
+        assert(not err)
+        if bytes == 0 then
+          print('note write 0 bytes')
+        end
+        assert(uv.fs_close(self.fd))
+      end)
+    end
+    return self
+  end
+
+  function fs:run(callback)
+    async(function()
+      for i, t in ipairs(task) do
+        local res = await(t)
+        if i == #task and callback then
+          callback(res)
+        end
+      end
+    end)()
+  end
+
+  return fs
+end
+
+local function async_read(filename, callback)
+  local fs = fs_module(filename)
+  fs:open('r'):size():read():run(callback)
+end
+
+local function async_write(filename, content)
+  local fs = fs_module(filename)
+  fs:open('w'):size():write(content):run()
+end
+
+return {
+  async = async,
+  await = await,
+  async_read = async_read,
+  async_write = async_write,
+}
diff --git a/lua/dashboard/entry.lua b/lua/dashboard/entry.lua
new file mode 100644
index 00000000..0b278d26
--- /dev/null
+++ b/lua/dashboard/entry.lua
@@ -0,0 +1,85 @@
+local api = vim.api
+local nvim_buf_set_keymap = api.nvim_buf_set_keymap
+local add_highlight = api.nvim_buf_add_highlight
+local buf_set_lines, buf_set_extmark = api.nvim_buf_set_lines, api.nvim_buf_set_extmark
+local ns = api.nvim_create_namespace('dashboard')
+local entry = {}
+
+local function box()
+  local t = {}
+  t.__index = t
+
+  function t:append(lines)
+    local bufnr = self.bufnr
+    if type(lines) == 'string' then
+      lines = { lines }
+    elseif type(lines) == 'function' then
+      lines = lines()
+    end
+    local count = api.nvim_buf_line_count(bufnr)
+    buf_set_lines(bufnr, count, -1, false, lines)
+
+    local obj = {}
+
+    function obj:iterate()
+      local index = 1
+      return function()
+        local line = lines[index]
+        index = index + 1
+        return line
+      end
+    end
+
+    function obj:hi(callback)
+      local iter = self:iterate()
+      local index = -1
+      for data in iter do
+        index = index + 1
+        if callback then
+          local hi = callback(data, count + index)
+          if hi then
+            add_highlight(bufnr, ns, hi, count + index, 0, -1)
+          end
+        end
+      end
+
+      return obj
+    end
+
+    function obj:tailbtn(callback)
+      local iter = self:iterate()
+      local index = -1
+      for data in iter do
+        index = index + 1
+        local btn, hi, action = callback(data)
+        if btn then
+          buf_set_extmark(bufnr, ns, count + index, 0, {
+            virt_text = { { btn, hi } },
+          })
+
+          nvim_buf_set_keymap(bufnr, 'n', btn, '', {
+            callback = function()
+              if type(action) == 'string' then
+                vim.cmd(action)
+              elseif type(action) == 'function' then
+                action()
+              end
+            end,
+          })
+        end
+      end
+
+      return obj
+    end
+
+    return setmetatable(obj, t)
+  end
+
+  return t
+end
+
+function entry:new(wininfo)
+  return setmetatable(wininfo, box())
+end
+
+return entry
diff --git a/lua/dashboard/events.lua b/lua/dashboard/events.lua
deleted file mode 100644
index a823c710..00000000
--- a/lua/dashboard/events.lua
+++ /dev/null
@@ -1,57 +0,0 @@
-local api, lsp, uv = vim.api, vim.lsp, vim.loop
-local au = {}
-
-function au.register_lsp_root(path)
-  api.nvim_create_autocmd('VimLeavePre', {
-    callback = function()
-      local projects = {}
-      for _, client in pairs(lsp.get_active_clients() or {}) do
-        local root_dir = client.config.root_dir
-        if root_dir and not vim.tbl_contains(projects, root_dir) then
-          table.insert(projects, root_dir)
-        end
-
-        for _, folder in pairs(client.workspace_folders or {}) do
-          if not vim.tbl_contains(projects, folder.name) then
-            table.insert(projects, folder.name)
-          end
-        end
-      end
-
-      if #projects == 0 then
-        return
-      end
-
-      -- callback hell holy shit but simply than write a async await lib
-      -- also I don't link to add a thirdpart plugin. this is just a small code
-      uv.fs_open(path, 'rs+', 438, function(err, fd)
-        assert(not err, err)
-        uv.fs_fstat(fd, function(err, stat)
-          assert(not err, err)
-          uv.fs_read(fd, stat.size, 0, function(err, data)
-            assert(not err, err)
-            local before = assert(loadstring(data))
-            local plist = before()
-            if plist and #plist > 10 then
-              plist = vim.list_slice(plist, 10)
-            end
-            plist = vim.tbl_filter(function(k)
-              return not vim.tbl_contains(projects, k)
-            end, plist or {})
-            plist = vim.list_extend(plist, projects)
-            local dump = 'return ' .. vim.inspect(plist)
-            uv.fs_write(fd, dump, 0, function(err, _)
-              assert(not err, err)
-              uv.fs_ftruncate(fd, #dump, function(err, _)
-                assert(not err, err)
-                uv.fs_close(fd)
-              end)
-            end)
-          end)
-        end)
-      end)
-    end,
-  })
-end
-
-return au
diff --git a/lua/dashboard/init.lua b/lua/dashboard/init.lua
index a0539b88..05d88585 100644
--- a/lua/dashboard/init.lua
+++ b/lua/dashboard/init.lua
@@ -1,280 +1,140 @@
 local api, fn = vim.api, vim.fn
-local utils = require('dashboard.utils')
-local ctx = {}
-local db = {}
-
-db.__index = db
-db.__newindex = function(t, k, v)
-  rawset(t, k, v)
-end
-
-local function clean_ctx()
-  for k, _ in pairs(ctx) do
-    ctx[k] = nil
-  end
-end
-
-local function cache_dir()
-  local dir = utils.path_join(vim.fn.stdpath('cache'), 'dashboard')
-  if fn.isdirectory(dir) == 0 then
-    fn.mkdir(dir, 'p')
-  end
-  return dir
-end
-
-local function cache_path()
-  local dir = cache_dir()
-  return utils.path_join(dir, 'cache')
-end
-
-local function conf_cache_path()
-  return utils.path_join(cache_dir(), 'conf')
-end
-
-local function default_options()
-  return {
-    theme = 'hyper',
-    disable_move = false,
-    shortcut_type = 'letter',
-    buffer_name = 'Dashboard',
-    change_to_vcs_root = false,
-    config = {
-      week_header = {
-        enable = false,
-        concat = nil,
-        append = nil,
-      },
-    },
-    hide = {
-      statusline = true,
-      tabline = true,
-    },
-    preview = {
-      command = '',
-      file_path = nil,
-      file_height = 0,
-      file_width = 0,
-    },
-  }
-end
+local nvim_create_autocmd = api.nvim_create_autocmd
+local opt_local = vim.opt_local
+local util = require('dashboard.util')
+local entry = require('dashboard.entry')
+local asmod = require('dashboard.async')
 
 local function buf_local()
-  local opts = {
-    ['bufhidden'] = 'wipe',
-    ['colorcolumn'] = '',
-    ['foldcolumn'] = '0',
-    ['matchpairs'] = '',
-    ['buflisted'] = false,
-    ['cursorcolumn'] = false,
-    ['cursorline'] = false,
-    ['list'] = false,
-    ['number'] = false,
-    ['relativenumber'] = false,
-    ['spell'] = false,
-    ['swapfile'] = false,
-    ['readonly'] = false,
-    ['filetype'] = 'dashboard',
-    ['wrap'] = false,
-    ['signcolumn'] = 'no',
-    ['winbar'] = '',
-  }
-  for opt, val in pairs(opts) do
-    vim.opt_local[opt] = val
-  end
-  if fn.has('nvim-0.9') == 1 then
-    vim.opt_local.stc = ''
-  end
+  opt_local.bufhidden = 'wipe'
+  opt_local.colorcolumn = ''
+  opt_local.foldcolumn = '0'
+  opt_local.matchpairs = ''
+  opt_local.buflisted = false
+  opt_local.cursorcolumn = false
+  opt_local.cursorline = false
+  opt_local.list = false
+  opt_local.number = false
+  opt_local.relativenumber = false
+  opt_local.spell = false
+  opt_local.swapfile = false
+  opt_local.readonly = false
+  opt_local.filetype = 'dashboard'
+  opt_local.wrap = false
+  opt_local.signcolumn = 'no'
+  opt_local.winbar = ''
+  opt_local.stc = ''
 end
 
-function db:new_file()
-  vim.cmd('enew')
-  if self.user_laststatus_value then
-    vim.opt_local.laststatus = self.user_laststatus_value
-    self.user_laststatus_value = nil
-  end
-
-  if self.user_tabline_value then
-    vim.opt_local.showtabline = self.user_showtabline_value
-    self.user_showtabline_value = nil
-  end
-end
+local function hide_options(hide)
+  local tbl = {}
 
--- cache the user options value restore after leave the dahsboard buffer
--- or use DashboardNewFile command
-function db:cache_ui_options(opts)
-  if opts.hide.statusline then
-    ---@diagnostic disable-next-line: param-type-mismatch
-    self.user_laststatus_value = vim.opt.laststatus:get()
+  if hide.statusline then
+    tbl.laststatus = util.get_global_option_value('laststatus')
     vim.opt.laststatus = 0
   end
-  if opts.hide.tabline then
-    ---@diagnostic disable-next-line: param-type-mismatch
-    self.user_tabline_value = vim.opt.showtabline:get()
-    vim.opt.showtabline = 0
-  end
-end
 
-function db:restore_options()
-  if self.user_cursor_line then
-    vim.opt.cursorline = self.user_cursor_line
-    self.user_cursor_line = nil
+  if hide.tabline then
+    tbl.showtabline = util.get_global_option_value('showtabline')
+    vim.opt.showtabline = 0
   end
 
-  if self.user_laststatus_value then
-    vim.opt.laststatus = tonumber(self.user_laststatus_value)
-    self.user_laststatus_value = nil
+  if hide.cursorline then
+    tbl.cursorline = util.get_global_option_value('cursorline')
   end
 
-  if self.user_tabline_value then
-    vim.opt.showtabline = tonumber(self.user_tabline_value)
-    self.user_tabline_value = nil
+  return function()
+    for option, val in pairs(tbl) do
+      vim.opt[option] = val
+    end
   end
 end
 
-function db:cache_opts()
-  if not self.opts then
+-- create dashboard instance
+local function instance(opt)
+  local mode = api.nvim_get_mode().mode
+  if mode == 'i' or not vim.bo.modifiable then
     return
   end
-  local uv = vim.loop
-  local path = conf_cache_path()
-  if self.opts.config.shortcut then
-    for _, item in pairs(self.opts.config.shortcut) do
-      if type(item.action) == 'function' then
-        ---@diagnostic disable-next-line: param-type-mismatch
-        local dump = assert(string.dump(item.action))
-        item.action = dump
-      end
-    end
-  end
 
-  if self.opts.config.project and type(self.opts.config.project.action) == 'function' then
-    ---@diagnostic disable-next-line: param-type-mismatch
-    local dump = assert(string.dump(self.opts.config.project.action))
-    self.opts.config.project.action = dump
-  end
-
-  if self.opts.config.center then
-    for _, item in pairs(self.opts.config.center) do
-      if type(item.action) == 'function' then
-        ---@diagnostic disable-next-line: param-type-mismatch
-        local dump = assert(string.dump(item.action))
-        item.action = dump
-      end
-    end
+  if not vim.o.hidden and vim.bo.modified then
+    --save before open
+    vim.cmd.write()
+    return
   end
 
-  if self.opts.config.footer and type(self.opts.config.footer) == 'function' then
-    ---@diagnostic disable-next-line: param-type-mismatch
-    local dump = assert(string.dump(self.opts.config.footer))
-    self.opts.config.footer = dump
+  local bufnr
+  if vim.fn.line2byte('$') ~= -1 then
+    bufnr = api.nvim_create_buf(false, true)
+  else
+    bufnr = api.nvim_get_current_buf()
   end
 
-  local dump = vim.json.encode(self.opts)
-  uv.fs_open(path, 'w+', tonumber('664', 8), function(err, fd)
-    assert(not err, err)
-    ---@diagnostic disable-next-line: redefined-local
-    uv.fs_write(fd, dump, 0, function(err, _)
-      assert(not err, err)
-      uv.fs_close(fd)
-    end)
-  end)
-end
-
-function db:get_opts(callback)
-  utils.async_read(
-    conf_cache_path(),
-    vim.schedule_wrap(function(data)
-      if not data or #data == 0 then
-        return
-      end
-      local obj = vim.json.decode(data)
-      if obj then
-        callback(obj)
-      end
-    end)
-  )
-end
-
-function db:load_theme(opts)
-  local config = vim.tbl_extend('force', opts.config, {
-    path = cache_path(),
-    bufnr = self.bufnr,
-    winid = self.winid,
-    confirm_key = opts.confirm_key or nil,
-    shortcut_type = opts.shortcut_type,
-    change_to_vcs_root = opts.change_to_vcs_root,
-  })
+  local winid = api.nvim_get_current_win()
+  api.nvim_win_set_buf(winid, bufnr)
 
-  -- api.nvim_buf_set_name(self.bufnr, utils.gen_bufname(opts.buffer_name))
+  buf_local()
 
-  if #opts.preview.command > 0 then
-    config = vim.tbl_extend('force', config, opts.preview)
+  local theme = require('dashboard.theme.' .. opt.theme)
+  if not theme or not theme.init then
+    vim.notify(
+      ('[Dashboard] Load theme error missed theme %s or %s.init function'):format(
+        opt.theme,
+        opt.theme
+      ),
+      vim.log.levels.ERROR
+    )
+    return
   end
-
-  require('dashboard.theme.' .. opts.theme)(config)
-  self:cache_ui_options(opts)
+  local restore = hide_options(opt.hide)
+  local e = entry:new({ winid = winid, bufnr = bufnr })
+  asmod.async(function()
+    e = asmod.await(theme.init(e, opt.theme_config))
+  end)()
 
   api.nvim_create_autocmd('VimResized', {
-    buffer = self.bufnr,
+    buffer = bufnr,
     callback = function()
-      require('dashboard.theme.' .. opts.theme)(config)
-      vim.bo[self.bufnr].modifiable = false
+      require('dashboard.theme.' .. opt.theme).init(entry, opt.theme_config)
+      vim.bo[bufnr].modifiable = false
     end,
   })
 
-  api.nvim_create_autocmd('BufEnter', {
-    callback = function(opt)
-      local bufs = api.nvim_list_bufs()
-      bufs = vim.tbl_filter(function(k)
-        return vim.bo[k].filetype == 'dashboard'
-      end, bufs)
-      if #bufs == 0 then
-        self:cache_opts()
-        self:restore_options()
-        clean_ctx()
-        pcall(api.nvim_del_autocmd, opt.id)
-      end
+  nvim_create_autocmd('BufLeave', {
+    buffer = bufnr,
+    callback = function()
+      restore()
     end,
-    desc = '[Dashboard] clean dashboard data reduce memory',
   })
 end
 
--- create dashboard instance
-function db:instance()
-  local mode = api.nvim_get_mode().mode
-  if mode == 'i' or not vim.bo.modifiable then
-    return
-  end
-
-  if not vim.o.hidden and vim.bo.modified then
-    --save before open
-    vim.cmd.write()
+local function setup(opt)
+  local default = {
+    theme = 'doom',
+    change_to_vcs_root = true,
+    hide = {
+      statusline = true,
+      tabline = true,
+      cursorline = true,
+    },
+    theme_config = {},
+  }
+  opt = vim.tbl_extend('force', default, opt or {})
+  if #opt.theme == 0 then
+    vim.notify('[Dashboard] Please config a theme ', vim.log.levels.WARN)
     return
   end
 
-  if vim.fn.line2byte('$') ~= -1 then
-    self.bufnr = api.nvim_create_buf(false, true)
-  else
-    self.bufnr = api.nvim_get_current_buf()
-  end
-
-  self.winid = api.nvim_get_current_win()
-  api.nvim_win_set_buf(self.winid, self.bufnr)
-
-  self.user_cursor_line = vim.opt.cursorline:get()
-  buf_local()
-  if self.opts then
-    self:load_theme(self.opts)
-  else
-    self:get_opts(function(obj)
-      self:load_theme(obj)
-    end)
-  end
-end
-
-function db.setup(opts)
-  opts = opts or {}
-  ctx.opts = vim.tbl_deep_extend('force', default_options(), opts)
+  nvim_create_autocmd('UIEnter', {
+    group = vim.api.nvim_create_augroup('Dashboard', { clear = true }),
+    callback = function()
+      if fn.argc() == 0 and fn.line2byte('$') == -1 then
+        instance(opt)
+      end
+    end,
+  })
 end
 
-return setmetatable(ctx, db)
+return {
+  setup = setup,
+}
diff --git a/lua/dashboard/theme/doom.lua b/lua/dashboard/theme/doom.lua
deleted file mode 100644
index 6798af06..00000000
--- a/lua/dashboard/theme/doom.lua
+++ /dev/null
@@ -1,188 +0,0 @@
-local api = vim.api
-local utils = require('dashboard.utils')
-
-local function generate_center(config)
-  local lines = {}
-  local center = config.center
-    or {
-      { desc = 'Please config your own center section', key = 'p' },
-    }
-
-  local counts = {}
-  for _, item in pairs(center) do
-    local count = item.keymap and #item.keymap or 0
-    local line = (item.icon or '') .. item.desc
-
-    if item.key then
-      line = line .. (' '):rep(#item.key + 4)
-      count = count + #item.key + 3
-      if type(item.action) == 'string' then
-        vim.keymap.set('n', item.key, function()
-          local dump = loadstring(item.action)
-          if not dump then
-            vim.cmd(item.action)
-          else
-            dump()
-          end
-        end, { buffer = config.bufnr, nowait = true, silent = true })
-      elseif type(item.action) == 'function' then
-        vim.keymap.set(
-          'n',
-          item.key,
-          item.action,
-          { buffer = config.bufnr, nowait = true, silent = true }
-        )
-      end
-    end
-
-    if item.keymap then
-      line = line .. (' '):rep(#item.keymap)
-    end
-
-    table.insert(lines, line)
-    table.insert(lines, '')
-    table.insert(counts, count)
-    table.insert(counts, 0)
-  end
-
-  lines = utils.element_align(lines)
-  lines = utils.center_align(lines)
-  for i, count in ipairs(counts) do
-    lines[i] = lines[i]:sub(1, #lines[i] - count)
-  end
-
-  local first_line = api.nvim_buf_line_count(config.bufnr)
-  api.nvim_buf_set_lines(config.bufnr, first_line, -1, false, lines)
-
-  if not config.center then
-    return
-  end
-
-  local ns = api.nvim_create_namespace('DashboardDoom')
-  local seed = 0
-  local pos_map = {}
-  for i = 1, #lines do
-    if lines[i]:find('%w') then
-      local idx = i == 1 and i or i - seed
-      seed = seed + 1
-      pos_map[i] = idx
-      local _, scol = lines[i]:find('%s+')
-      local ecol = scol + (config.center[idx].icon and #config.center[idx].icon or 0)
-
-      if config.center[idx].icon then
-        api.nvim_buf_add_highlight(
-          config.bufnr,
-          0,
-          config.center[idx].icon_hl or 'DashboardIcon',
-          first_line + i - 1,
-          0,
-          ecol
-        )
-      end
-
-      api.nvim_buf_add_highlight(
-        config.bufnr,
-        0,
-        config.center[idx].desc_hl or 'DashboardDesc',
-        first_line + i - 1,
-        ecol,
-        -1
-      )
-
-      if config.center[idx].key then
-        local virt_tbl = {}
-        if config.center[idx].keymap then
-          table.insert(virt_tbl, { config.center[idx].keymap, 'DashboardShortCut' })
-        end
-        table.insert(
-          virt_tbl,
-          { ' [' .. config.center[idx].key .. ']', config.center[idx].key_hl or 'DashboardKey' }
-        )
-        api.nvim_buf_set_extmark(config.bufnr, ns, first_line + i - 1, 0, {
-          virt_text_pos = 'eol',
-          virt_text = virt_tbl,
-        })
-      end
-    end
-  end
-
-  local line = api.nvim_buf_get_lines(config.bufnr, first_line, first_line + 1, false)[1]
-  local col = line:find('%w')
-  col = col and col - 1 or 9999
-  api.nvim_win_set_cursor(config.winid, { first_line + 1, col })
-
-  local bottom = api.nvim_buf_line_count(config.bufnr)
-  vim.defer_fn(function()
-    local before = 0
-    api.nvim_create_autocmd('CursorMoved', {
-      buffer = config.bufnr,
-      callback = function()
-        local curline = api.nvim_win_get_cursor(0)[1]
-        if curline < first_line + 1 then
-          curline = bottom - 1
-        elseif curline > bottom - 1 then
-          curline = first_line + 1
-        elseif not api.nvim_get_current_line():find('%w') then
-          curline = curline + (before > curline and -1 or 1)
-        end
-        before = curline
-        api.nvim_win_set_cursor(config.winid, { curline, col })
-      end,
-    })
-  end, 0)
-
-  vim.keymap.set('n', config.confirm_key or '<CR>', function()
-    local curline = api.nvim_win_get_cursor(0)[1]
-    local index = pos_map[curline - first_line]
-    if index and config.center[index].action then
-      if type(config.center[index].action) == 'string' then
-        local dump = loadstring(config.center[index].action)
-        if not dump then
-          vim.cmd(config.center[index].action)
-        else
-          dump()
-        end
-      elseif type(config.center[index].action) == 'function' then
-        config.center[index].action()
-      else
-        print('Error with action, check your config')
-      end
-    end
-  end, { buffer = config.bufnr, nowait = true, silent = true })
-end
-
-local function generate_footer(config)
-  local first_line = api.nvim_buf_line_count(config.bufnr)
-  local footer = { '', '', 'neovim loaded ' .. utils.get_packages_count() .. ' packages' }
-  if config.footer then
-    if type(config.footer) == 'function' then
-      footer = config.footer()
-    elseif type(config.footer) == 'string' then
-      local dump = loadstring(config.footer)
-      if dump then
-        footer = dump()
-      end
-    elseif type(config.footer) == 'table' then
-      footer = config.footer
-    end
-  end
-  api.nvim_buf_set_lines(config.bufnr, first_line, -1, false, utils.center_align(footer))
-  for i = 1, #footer do
-    api.nvim_buf_add_highlight(config.bufnr, 0, 'DashboardFooter', first_line + i - 1, 0, -1)
-  end
-end
-
----@private
-local function theme_instance(config)
-  require('dashboard.theme.header').generate_header(config)
-  generate_center(config)
-  generate_footer(config)
-  api.nvim_set_option_value('modifiable', false, { buf = config.bufnr })
-  api.nvim_set_option_value('modified', false, { buf = config.bufnr })
-end
-
-return setmetatable({}, {
-  __call = function(_, t)
-    return theme_instance(t)
-  end,
-})
diff --git a/lua/dashboard/theme/header.lua b/lua/dashboard/theme/header.lua
deleted file mode 100644
index 23328cd4..00000000
--- a/lua/dashboard/theme/header.lua
+++ /dev/null
@@ -1,134 +0,0 @@
-local api = vim.api
-local utils = require('dashboard.utils')
-
-local function week_ascii_text()
-  return {
-    ['Monday'] = {
-      '',
-      '███╗   ███╗ ██████╗ ███╗   ██╗██████╗  █████╗ ██╗   ██╗',
-      '████╗ ████║██╔═══██╗████╗  ██║██╔══██╗██╔══██╗╚██╗ ██╔╝',
-      '██╔████╔██║██║   ██║██╔██╗ ██║██║  ██║███████║ ╚████╔╝ ',
-      '██║╚██╔╝██║██║   ██║██║╚██╗██║██║  ██║██╔══██║  ╚██╔╝  ',
-      '██║ ╚═╝ ██║╚██████╔╝██║ ╚████║██████╔╝██║  ██║   ██║   ',
-      '╚═╝     ╚═╝ ╚═════╝ ╚═╝  ╚═══╝╚═════╝ ╚═╝  ╚═╝   ╚═╝   ',
-      '',
-    },
-    ['Tuesday'] = {
-      '',
-      '████████╗██╗   ██╗███████╗███████╗██████╗  █████╗ ██╗   ██╗',
-      '╚══██╔══╝██║   ██║██╔════╝██╔════╝██╔══██╗██╔══██╗╚██╗ ██╔╝',
-      '   ██║   ██║   ██║█████╗  ███████╗██║  ██║███████║ ╚████╔╝ ',
-      '   ██║   ██║   ██║██╔══╝  ╚════██║██║  ██║██╔══██║  ╚██╔╝  ',
-      '   ██║   ╚██████╔╝███████╗███████║██████╔╝██║  ██║   ██║   ',
-      '   ╚═╝    ╚═════╝ ╚══════╝╚══════╝╚═════╝ ╚═╝  ╚═╝   ╚═╝   ',
-      '',
-    },
-    ['Wednesday'] = {
-      '',
-      '██╗    ██╗███████╗██████╗ ███╗   ██╗███████╗███████╗██████╗  █████╗ ██╗   ██╗',
-      '██║    ██║██╔════╝██╔══██╗████╗  ██║██╔════╝██╔════╝██╔══██╗██╔══██╗╚██╗ ██╔╝',
-      '██║ █╗ ██║█████╗  ██║  ██║██╔██╗ ██║█████╗  ███████╗██║  ██║███████║ ╚████╔╝ ',
-      '██║███╗██║██╔══╝  ██║  ██║██║╚██╗██║██╔══╝  ╚════██║██║  ██║██╔══██║  ╚██╔╝  ',
-      '╚███╔███╔╝███████╗██████╔╝██║ ╚████║███████╗███████║██████╔╝██║  ██║   ██║   ',
-      '',
-    },
-    ['Thursday'] = {
-      '',
-      '████████╗██╗  ██╗██╗   ██╗██████╗ ███████╗██████╗  █████╗ ██╗   ██╗',
-      '╚══██╔══╝██║  ██║██║   ██║██╔══██╗██╔════╝██╔══██╗██╔══██╗╚██╗ ██╔╝',
-      '   ██║   ███████║██║   ██║██████╔╝███████╗██║  ██║███████║ ╚████╔╝ ',
-      '   ██║   ██╔══██║██║   ██║██╔══██╗╚════██║██║  ██║██╔══██║  ╚██╔╝  ',
-      '   ██║   ██║  ██║╚██████╔╝██║  ██║███████║██████╔╝██║  ██║   ██║   ',
-      '   ╚═╝   ╚═╝  ╚═╝ ╚═════╝ ╚═╝  ╚═╝╚══════╝╚═════╝ ╚═╝  ╚═╝   ╚═╝   ',
-      '',
-    },
-    ['Friday'] = {
-      '',
-      '███████╗██████╗ ██╗██████╗  █████╗ ██╗   ██╗',
-      '██╔════╝██╔══██╗██║██╔══██╗██╔══██╗╚██╗ ██╔╝',
-      '█████╗  ██████╔╝██║██║  ██║███████║ ╚████╔╝ ',
-      '██╔══╝  ██╔══██╗██║██║  ██║██╔══██║  ╚██╔╝  ',
-      '██║     ██║  ██║██║██████╔╝██║  ██║   ██║   ',
-      '╚═╝     ╚═╝  ╚═╝╚═╝╚═════╝ ╚═╝  ╚═╝   ╚═╝   ',
-      '',
-    },
-    ['Saturday'] = {
-      '',
-      '███████╗ █████╗ ████████╗██╗   ██╗██████╗ ██████╗  █████╗ ██╗   ██╗',
-      '██╔════╝██╔══██╗╚══██╔══╝██║   ██║██╔══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝',
-      '███████╗███████║   ██║   ██║   ██║██████╔╝██║  ██║███████║ ╚████╔╝ ',
-      '╚════██║██╔══██║   ██║   ██║   ██║██╔══██╗██║  ██║██╔══██║  ╚██╔╝  ',
-      '███████║██║  ██║   ██║   ╚██████╔╝██║  ██║██████╔╝██║  ██║   ██║   ',
-      '╚══════╝╚═╝  ╚═╝   ╚═╝    ╚═════╝ ╚═╝  ╚═╝╚═════╝ ╚═╝  ╚═╝   ╚═╝   ',
-      '',
-    },
-    ['Sunday'] = {
-      '',
-      '███████╗██╗   ██╗███╗   ██╗██████╗  █████╗ ██╗   ██╗',
-      '██╔════╝██║   ██║████╗  ██║██╔══██╗██╔══██╗╚██╗ ██╔╝',
-      '███████╗██║   ██║██╔██╗ ██║██║  ██║███████║ ╚████╔╝ ',
-      '╚════██║██║   ██║██║╚██╗██║██║  ██║██╔══██║  ╚██╔╝  ',
-      '███████║╚██████╔╝██║ ╚████║██████╔╝██║  ██║   ██║   ',
-      '╚══════╝ ╚═════╝ ╚═╝  ╚═══╝╚═════╝ ╚═╝  ╚═╝   ╚═╝   ',
-      '',
-    },
-  }
-end
-
-local function default_header()
-  return {
-    '',
-    ' ██████╗  █████╗ ███████╗██╗  ██╗██████╗  ██████╗  █████╗ ██████╗ ██████╗  ',
-    ' ██╔══██╗██╔══██╗██╔════╝██║  ██║██╔══██╗██╔═══██╗██╔══██╗██╔══██╗██╔══██╗ ',
-    ' ██║  ██║███████║███████╗███████║██████╔╝██║   ██║███████║██████╔╝██║  ██║ ',
-    ' ██║  ██║██╔══██║╚════██║██╔══██║██╔══██╗██║   ██║██╔══██║██╔══██╗██║  ██║ ',
-    ' ██████╔╝██║  ██║███████║██║  ██║██████╔╝╚██████╔╝██║  ██║██║  ██║██████╔╝ ',
-    ' ╚═════╝ ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝╚═════╝  ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝  ',
-    '',
-  }
-end
-
-local function week_header(concat, append)
-  local week = week_ascii_text()
-  local daysoftheweek =
-    { 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' }
-  local day = daysoftheweek[os.date('*t').wday]
-  local tbl = week[day]
-  table.insert(tbl, os.date('%Y-%m-%d %H:%M:%S ') .. (concat or ''))
-  if append then
-    vim.list_extend(tbl, append)
-  end
-  table.insert(tbl, '')
-  return tbl
-end
-
-local function generate_header(config)
-  if not vim.bo[config.bufnr].modifiable then
-    vim.bo[config.bufnr].modifiable = true
-  end
-  if not config.command then
-    local header = config.week_header
-        and config.week_header.enable
-        and week_header(config.week_header.concat, config.week_header.append)
-      or (config.header or default_header())
-    api.nvim_buf_set_lines(config.bufnr, 0, -1, false, utils.center_align(header))
-
-    for i, _ in ipairs(header) do
-      vim.api.nvim_buf_add_highlight(config.bufnr, 0, 'DashboardHeader', i - 1, 0, -1)
-    end
-    return
-  end
-
-  local empty_table = utils.generate_empty_table(config.file_height + 4)
-  api.nvim_buf_set_lines(config.bufnr, 0, -1, false, utils.center_align(empty_table))
-  local preview = require('dashboard.preview')
-  preview:open_preview({
-    width = config.file_width,
-    height = config.file_height,
-    cmd = config.command .. ' ' .. config.file_path,
-  })
-end
-
-return {
-  generate_header = generate_header,
-}
diff --git a/lua/dashboard/theme/hyper.lua b/lua/dashboard/theme/hyper.lua
deleted file mode 100644
index 16368a28..00000000
--- a/lua/dashboard/theme/hyper.lua
+++ /dev/null
@@ -1,443 +0,0 @@
-local api, keymap = vim.api, vim.keymap
-local utils = require('dashboard.utils')
-local ns = api.nvim_create_namespace('dashboard')
-
-local function gen_shortcut(config)
-  local shortcut = config.shortcut
-    or {
-      { desc = '[  Github]', group = 'DashboardShortCut' },
-      { desc = '[  glepnir]', group = 'DashboardShortCut' },
-      { desc = '[  0.2.3]', group = 'DashboardShortCut' },
-    }
-
-  if vim.tbl_isempty(shortcut) then
-    shortcut = {}
-  end
-
-  local lines = ''
-  for _, item in pairs(shortcut) do
-    local str = item.icon and item.icon .. item.desc or item.desc
-    if item.key then
-      str = str .. '[' .. item.key .. ']'
-    end
-    lines = lines .. '  ' .. str
-  end
-
-  local first_line = api.nvim_buf_line_count(config.bufnr)
-  api.nvim_buf_set_lines(config.bufnr, first_line, -1, false, utils.center_align({ lines }))
-
-  local line = api.nvim_buf_get_lines(config.bufnr, first_line, -1, false)[1]
-  local start = line:find('[^%s]') - 1
-  for _, item in pairs(shortcut) do
-    local _end = start + (item.icon and #(item.icon .. item.desc) or #item.desc)
-    if item.key then
-      _end = _end + api.nvim_strwidth(item.key) + 2
-      keymap.set('n', item.key, function()
-        if type(item.action) == 'string' then
-          local dump = loadstring(item.action)
-          if not dump then
-            vim.cmd(item.action)
-          else
-            dump()
-          end
-        elseif type(item.action) == 'function' then
-          item.action()
-        end
-      end, { buffer = config.bufnr, nowait = true, silent = true })
-    end
-
-    api.nvim_buf_add_highlight(
-      config.bufnr,
-      0,
-      item.group or 'DashboardShortCut',
-      first_line,
-      start,
-      _end
-    )
-
-    if item.icon then
-      api.nvim_buf_add_highlight(
-        config.bufnr,
-        0,
-        item.icon_hl or 'DashboardShortCutIcon',
-        first_line,
-        start,
-        start + #item.icon
-      )
-    end
-    start = _end + 2
-  end
-end
-
-local function load_packages(config)
-  local packages = config.packages or {
-    enable = true,
-  }
-  if not packages.enable then
-    return
-  end
-
-  local lines = {
-    '',
-    'neovim loaded ' .. utils.get_packages_count() .. ' packages',
-  }
-
-  local first_line = api.nvim_buf_line_count(config.bufnr)
-  api.nvim_buf_set_lines(config.bufnr, first_line, -1, false, utils.center_align(lines))
-
-  for i, _ in pairs(lines) do
-    api.nvim_buf_add_highlight(config.bufnr, 0, 'Comment', first_line + i - 1, 0, -1)
-  end
-end
-
-local function reverse(tbl)
-  for i = 1, math.floor(#tbl / 2) do
-    tbl[i], tbl[#tbl - i + 1] = tbl[#tbl - i + 1], tbl[i]
-  end
-end
-
-local function project_list(config, callback)
-  config.project = vim.tbl_extend('force', {
-    limit = 8,
-    enable = true,
-    icon = '󰏓 ',
-    icon_hl = 'DashboardRecentProjectIcon',
-    action = 'Telescope find_files cwd=',
-    label = ' Recent Projects:',
-  }, config.project or {})
-
-  local function read_project(data)
-    local res = {}
-    local dump = assert(loadstring(data))
-    local list = dump()
-    if list then
-      list = vim.list_slice(list, #list - config.project.limit)
-    end
-    for _, dir in ipairs(list or {}) do
-      dir = dir:gsub(vim.env.HOME, '~')
-      table.insert(res, (' '):rep(3) .. ' ' .. dir)
-    end
-
-    if #res == 0 then
-      table.insert(res, (' '):rep(3) .. ' empty project')
-    else
-      reverse(res)
-    end
-
-    table.insert(res, 1, config.project.icon .. config.project.label)
-    table.insert(res, 1, '')
-    table.insert(res, '')
-    return res
-  end
-
-  utils.async_read(
-    config.path,
-    vim.schedule_wrap(function(data)
-      local res = {}
-      if config.project.enable then
-        res = read_project(data)
-      end
-      callback(res)
-    end)
-  )
-end
-
-local function mru_list(config)
-  config.mru = vim.tbl_extend('force', {
-    icon = ' ',
-    limit = 10,
-    icon_hl = 'DashboardMruIcon',
-    label = ' Most Recent Files:',
-  }, config.mru or {})
-
-  local list = {
-    config.mru.icon .. config.mru.label,
-  }
-
-  local groups = {}
-  local mlist = utils.get_mru_list()
-
-  for _, file in pairs(vim.list_slice(mlist, 1, config.mru.limit)) do
-    local ft = vim.filetype.match({ filename = file })
-    local icon, group = utils.get_icon(ft)
-    icon = icon or ' '
-    if not utils.is_win then
-      file = file:gsub(vim.env.HOME, '~')
-    end
-    file = icon .. ' ' .. file
-    table.insert(groups, { #icon, group })
-    table.insert(list, (' '):rep(3) .. file)
-  end
-
-  if #list == 1 then
-    table.insert(list, (' '):rep(3) .. ' empty files')
-  end
-  return list, groups
-end
-
-local function letter_hotkey(config)
-  local list = { 106, 107 }
-  for _, item in pairs(config.shortcut or {}) do
-    if item.key then
-      table.insert(list, item.key:byte())
-    end
-  end
-  math.randomseed(os.time())
-  return function()
-    while true do
-      local key = math.random(97, 122)
-      if not vim.tbl_contains(list, key) then
-        table.insert(list, key)
-        return string.char(key)
-      end
-    end
-  end
-end
-
-local function number_hotkey()
-  local start = 0
-  return function()
-    start = start + 1
-    return start
-  end
-end
-
-local function gen_hotkey(config)
-  if config.shortcut_type == 'number' then
-    return number_hotkey()
-  end
-  return letter_hotkey(config)
-end
-
-local function map_key(config, key, content)
-  keymap.set('n', key, function()
-    local text = content or api.nvim_get_current_line()
-    local scol = utils.is_win and text:find('%w') or text:find('%p')
-    text = text:sub(scol)
-    local path = text:sub(1, text:find('%w(%s+)$'))
-    path = vim.fs.normalize(path)
-    if vim.fn.isdirectory(path) == 1 then
-      vim.cmd('lcd ' .. path)
-      if type(config.project.action) == 'function' then
-        config.project.action(path)
-      elseif type(config.project.action) == 'string' then
-        local dump = loadstring(config.project.action)
-        if not dump then
-          vim.cmd(config.project.action .. path)
-        else
-          dump(path)
-        end
-      end
-    else
-      vim.cmd('edit ' .. path)
-      local root = utils.get_vcs_root()
-      if not config.change_to_vcs_root then
-        return
-      end
-      if #root > 0 then
-        vim.cmd('lcd ' .. vim.fn.fnamemodify(root[#root], ':h'))
-      else
-        vim.cmd('lcd ' .. vim.fn.fnamemodify(path, ':h'))
-      end
-    end
-  end, { buffer = config.bufnr, silent = true, nowait = true })
-end
-
-local function gen_center(plist, config)
-  local mlist, mgroups = mru_list(config)
-  local plist_len = #plist
-  if plist_len == 0 then
-    plist[#plist + 1] = ''
-    plist_len = 1
-  end
-  ---@diagnostic disable-next-line: param-type-mismatch
-  vim.list_extend(plist, mlist)
-  local max_len = utils.get_max_len(plist)
-  if max_len <= 40 then
-    local fill = (' '):rep(math.floor(vim.o.columns / 4))
-    for i, v in pairs(plist) do
-      plist[i] = v .. fill
-    end
-  end
-
-  plist = utils.element_align(plist)
-  plist = utils.center_align(plist)
-  local first_line = api.nvim_buf_line_count(config.bufnr)
-  api.nvim_buf_set_lines(config.bufnr, first_line, -1, false, plist)
-
-  local start_col = plist[plist_len + 2]:find('[^%s]') - 1
-  local _, scol = plist[2]:find('%S')
-
-  local hotkey = gen_hotkey(config)
-
-  api.nvim_buf_add_highlight(config.bufnr, 0, 'DashboardProjectTitle', first_line + 1, 0, -1)
-  api.nvim_buf_add_highlight(
-    config.bufnr,
-    0,
-    'DashboardProjectTitleIcon',
-    first_line + 1,
-    0,
-    scol + #config.project.icon
-  )
-
-  for i = 3, plist_len do
-    api.nvim_buf_add_highlight(
-      config.bufnr,
-      0,
-      'DashboardProjectIcon',
-      first_line + i - 1,
-      0,
-      start_col + 3
-    )
-    api.nvim_buf_add_highlight(
-      config.bufnr,
-      0,
-      'DashboardFiles',
-      first_line + i - 1,
-      start_col + 3,
-      -1
-    )
-    local text = api.nvim_buf_get_lines(config.bufnr, first_line + i - 1, first_line + i, false)[1]
-    if text and text:find('%w') and not text:find('empty') then
-      local key = tostring(hotkey())
-      api.nvim_buf_set_extmark(config.bufnr, ns, first_line + i - 1, 0, {
-        virt_text = { { key, 'DashboardShortCut' } },
-        virt_text_pos = 'eol',
-      })
-      map_key(config, key, text)
-    end
-  end
-
-  -- initialize the cursor pos
-  api.nvim_win_set_cursor(config.winid, { first_line + 3, start_col + 4 })
-
-  api.nvim_buf_add_highlight(config.bufnr, 0, 'DashboardMruTitle', first_line + plist_len, 0, -1)
-  api.nvim_buf_add_highlight(
-    config.bufnr,
-    0,
-    'DashboardMruIcon',
-    first_line + plist_len,
-    0,
-    scol + #config.mru.icon
-  )
-
-  for i, data in pairs(mgroups) do
-    local len, group = unpack(data)
-    api.nvim_buf_add_highlight(
-      config.bufnr,
-      0,
-      group,
-      first_line + i + plist_len,
-      start_col,
-      start_col + len
-    )
-    api.nvim_buf_add_highlight(
-      config.bufnr,
-      0,
-      'DashboardFiles',
-      first_line + i + plist_len,
-      start_col + len,
-      -1
-    )
-
-    local text = api.nvim_buf_get_lines(
-      config.bufnr,
-      first_line + i + plist_len,
-      first_line + i + plist_len + 1,
-      false
-    )[1]
-    if text and text:find('%w') then
-      local key = tostring(hotkey())
-      api.nvim_buf_set_extmark(config.bufnr, ns, first_line + i + plist_len, 0, {
-        virt_text = { { key, 'DashboardShortCut' } },
-        virt_text_pos = 'eol',
-      })
-      map_key(config, key, text)
-    end
-  end
-end
-
-local function gen_footer(config)
-  local footer = {
-    '',
-    ' 🚀 Sharp tools make good work.',
-  }
-
-  if type(config.footer) == 'string' then
-    local dump = loadstring(config.footer)
-    if dump then
-      footer = dump()
-    end
-  elseif type(config.footer) == 'function' then
-    footer = config.footer()
-  elseif type(config.footer) == 'table' then
-    footer = config.footer
-  end
-
-  local first_line = api.nvim_buf_line_count(config.bufnr)
-  api.nvim_buf_set_lines(config.bufnr, first_line, -1, false, utils.center_align(footer))
-
-  ---@diagnostic disable-next-line: param-type-mismatch
-  for i, _ in pairs(footer) do
-    api.nvim_buf_add_highlight(config.bufnr, 0, 'DashboardFooter', first_line + i - 1, 0, -1)
-  end
-end
-
-local function project_delete()
-  api.nvim_create_user_command('DbProjectDelete', function(args)
-    local path = utils.path_join(vim.fn.stdpath('cache'), 'dashboard', 'cache')
-    utils.async_read(
-      path,
-      vim.schedule_wrap(function(data)
-        local dump = assert(loadstring(data))
-        local list = dump()
-        local count = tonumber(args.args)
-        if vim.tbl_count(list) < count then
-          return
-        end
-        list = vim.list_slice(list, count + 1)
-        local str = string.dump(assert(loadstring('return ' .. vim.inspect(list))))
-        local handle = io.open(path, 'w+')
-        if not handle then
-          return
-        end
-        handle:write(str)
-        handle:close()
-      end)
-    )
-  end, {
-    nargs = '+',
-  })
-end
-
-local function theme_instance(config)
-  project_list(config, function(plist)
-    if not api.nvim_buf_is_valid(config.bufnr) then
-      return
-    end
-    if config.disable_move then
-      utils.disable_move_key(config.bufnr)
-    end
-    require('dashboard.theme.header').generate_header(config)
-    gen_shortcut(config)
-    load_packages(config)
-    gen_center(plist, config)
-    gen_footer(config)
-    map_key(config, config.confirm_key or '<CR>')
-    require('dashboard.events').register_lsp_root(config.path)
-    local size = math.floor(vim.o.lines / 2)
-      - math.ceil(api.nvim_buf_line_count(config.bufnr) / 2)
-      - 2
-    local fill = utils.generate_empty_table(size)
-    api.nvim_buf_set_lines(config.bufnr, 0, 0, false, fill)
-    vim.bo[config.bufnr].modifiable = false
-    vim.bo[config.bufnr].modified = false
-    project_delete()
-  end)
-end
-
-return setmetatable({}, {
-  __call = function(_, t)
-    theme_instance(t)
-  end,
-})
diff --git a/lua/dashboard/util.lua b/lua/dashboard/util.lua
new file mode 100644
index 00000000..6868e3ce
--- /dev/null
+++ b/lua/dashboard/util.lua
@@ -0,0 +1,129 @@
+local uv, api, fn = vim.loop, vim.api, vim.fn
+local util = {}
+
+local function strscreenwidth(str)
+  return vim.fn.strdisplaywidth(str)
+end
+
+---get os path separator
+---@return string
+function util.path_sep()
+  local slash = false
+  local path_sep = '/'
+  if fn.exists('+shellslash') == 1 then
+    ---@diagnostic disable-next-line: param-type-mismatch
+    slash = vim.opt.shellslash:get()
+  end
+  local iswin = uv.os_uname().version:match('Windows')
+  if iswin and not slash then
+    path_sep = '\\'
+  end
+  return path_sep
+end
+
+---join path
+---@return string
+function util.path_join(...)
+  return table.concat({ ... }, util.path_sep())
+end
+
+---get dashboard cache path
+---@return string
+function util.cache_path()
+  local dir = util.path_join(fn.stdpath('cache'), 'dashboard')
+  if fn.isdirectory(dir) == 0 then
+    fn.mkdir(dir, 'p')
+  end
+  return dir
+end
+
+function util.get_global_option_value(name)
+  return api.nvim_get_option_value(name, { scope = 'global' })
+end
+
+local function get_max_len(lines)
+  local cells = {}
+  vim.tbl_map(function(item)
+    cells[#cells + 1] = strscreenwidth(item)
+  end, lines)
+
+  table.sort(cells)
+  return cells[#cells]
+end
+
+---tail align lines insert spaces at the every item of lines tail
+---@param lines table
+---@return table
+function util.tail_align(lines)
+  local max = get_max_len(lines)
+  local res = {}
+
+  for _, item in ipairs(lines) do
+    item = item .. (' '):rep(max - strscreenwidth(item))
+    res[#res + 1] = item
+  end
+
+  return res
+end
+
+---head align lines insert spaces at the every item of lines head
+---according nvim screen width
+---@param lines table
+---@return table
+function util.center_align(lines)
+  local screen_center = math.floor(bit.rshift(vim.o.columns, 1))
+  local new = {}
+  for _, line in ipairs(lines) do
+    local line_center = math.floor(bit.rshift(strscreenwidth(line), 1))
+    local size = screen_center - line_center
+    line = (' '):rep(size) .. line
+    new[#new + 1] = line
+  end
+
+  return new
+end
+
+---@param ft string
+---@return table|nil
+function util.get_devicons_icon(ft)
+  local ok, devicons = pcall(require, 'nvim-web-devicons')
+  if not ok then
+    return nil
+  end
+  return devicons.get_icon_by_filetype(ft, { default = true })
+end
+
+---generate an empty table by given capacity
+---@param capacity number
+---@return table
+function util.generate_empty_table(capacity)
+  local res = {}
+  if capacity == 0 then
+    return res
+  end
+
+  for _ = 1, capacity do
+    res[#res + 1] = ''
+  end
+  return res
+end
+
+---@param bufnr number
+---@return string|nil
+function util.get_vcs_root(bufnr)
+  bufnr = bufnr or 0
+  local path = fn.fnamemodify(api.nvim_buf_get_name(bufnr), ':p:h')
+  local patterns = { '.git', '.hg', '.bzr', '.svn' }
+  for _, pattern in ipairs(patterns) do
+    local root = vim.fs.find(pattern, { path = path, upward = true, stop = vim.env.HOME })
+    if root then
+      local res = root[#root]
+      local sep = util.path_sep()
+      res = vim.split(res, sep)
+      res[#res] = nil
+      return table.concat(res, sep)
+    end
+  end
+end
+
+return util
diff --git a/lua/dashboard/utils.lua b/lua/dashboard/utils.lua
deleted file mode 100644
index ee57642f..00000000
--- a/lua/dashboard/utils.lua
+++ /dev/null
@@ -1,165 +0,0 @@
-local uv = vim.loop
-local utils = {}
-
-utils.is_win = uv.os_uname().version:match('Windows')
-
-function utils.path_join(...)
-  local path_sep = utils.is_win and '\\' or '/'
-  return table.concat({ ... }, path_sep)
-end
-
-function utils.element_align(tbl)
-  local lens = {}
-  vim.tbl_map(function(k)
-    table.insert(lens, vim.api.nvim_strwidth(k))
-  end, tbl)
-  table.sort(lens)
-  local max = lens[#lens]
-  local res = {}
-  for _, item in pairs(tbl) do
-    local len = vim.api.nvim_strwidth(item)
-    local times = math.floor((max - len) / vim.api.nvim_strwidth(' '))
-    item = item .. (' '):rep(times)
-    table.insert(res, item)
-  end
-  return res
-end
-
-function utils.get_max_len(contents)
-  vim.validate({
-    contents = { contents, 't' },
-  })
-  local cells = {}
-  for _, v in pairs(contents) do
-    table.insert(cells, vim.api.nvim_strwidth(v))
-  end
-  table.sort(cells)
-  return cells[#cells]
-end
-
--- draw the graphics into the screen center
-function utils.center_align(tbl)
-  vim.validate({
-    tbl = { tbl, 'table' },
-  })
-  local function fill_sizes(lines)
-    local fills = {}
-    for _, line in pairs(lines) do
-      table.insert(fills, math.floor((vim.o.columns - vim.api.nvim_strwidth(line)) / 2))
-    end
-    return fills
-  end
-
-  local centered_lines = {}
-  local fills = fill_sizes(tbl)
-
-  for i = 1, #tbl do
-    local fill_line = (' '):rep(fills[i]) .. tbl[i]
-    table.insert(centered_lines, fill_line)
-  end
-
-  return centered_lines
-end
-
-function utils.get_icon(ft)
-  local ok, devicons = pcall(require, 'nvim-web-devicons')
-  if not ok then
-    vim.notify('[dashboard.nvim] not found nvim-web-devicons')
-    return nil
-  end
-  return devicons.get_icon_by_filetype(ft, { default = true })
-end
-
-function utils.read_project_cache(path)
-  local fd = assert(uv.fs_open(path, 'r', tonumber('644', 8)))
-  local stat = uv.fs_fstat(fd)
-  local chunk = uv.fs_read(fd, stat.size, 0)
-  local dump = assert(loadstring(chunk))
-  return dump()
-end
-
-function utils.async_read(path, callback)
-  uv.fs_open(path, 'a+', 438, function(err, fd)
-    assert(not err, err)
-    uv.fs_fstat(fd, function(err, stat)
-      assert(not err, err)
-      uv.fs_read(fd, stat.size, 0, function(err, data)
-        assert(not err, err)
-        uv.fs_close(fd, function(err)
-          assert(not err, err)
-          callback(data)
-        end)
-      end)
-    end)
-  end)
-end
-
-function utils.disable_move_key(bufnr)
-  local keys = { 'w', 'f', 'b', 'h', 'j', 'k', 'l', '<Up>', '<Down>', '<Left>', '<Right>' }
-  vim.tbl_map(function(k)
-    vim.keymap.set('n', k, '<Nop>', { buffer = bufnr })
-  end, keys)
-end
-
---- return the most recently files list
-function utils.get_mru_list()
-  local mru = {}
-  for _, file in pairs(vim.v.oldfiles or {}) do
-    if file and vim.fn.filereadable(file) == 1 then
-      table.insert(mru, file)
-    end
-  end
-  return mru
-end
-
-function utils.get_packages_count()
-  local count = 0
-  ---@diagnostic disable-next-line: undefined-global
-  if packer_plugins then
-    ---@diagnostic disable-next-line: undefined-global
-    count = #vim.tbl_keys(packer_plugins)
-  end
-  local status, lazy = pcall(require, 'lazy')
-  if status then
-    count = lazy.stats().count
-  end
-  return count
-end
-
---- generate an empty table by length
-function utils.generate_empty_table(length)
-  local empty_tbl = {}
-  if length == 0 then
-    return empty_tbl
-  end
-
-  for _ = 1, length do
-    table.insert(empty_tbl, '')
-  end
-  return empty_tbl
-end
-
-function utils.generate_truncateline(cells)
-  local char = '┉'
-  return char:rep(math.floor(cells / vim.api.nvim_strwidth(char)))
-end
-
-function utils.get_vcs_root(buf)
-  buf = buf or 0
-  local path = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ':p:h')
-  local patterns = { '.git', '.hg', '.bzr', '.svn' }
-  for _, pattern in pairs(patterns) do
-    local root = vim.fs.find(pattern, { path = path, upward = true, stop = vim.env.HOME })
-    if root then
-      return root
-    end
-  end
-end
-
-local index = 0
-function utils.gen_bufname(prefix)
-  index = index + 1
-  return prefix .. '-' .. index
-end
-
-return utils
diff --git a/min.lua b/min.lua
new file mode 100644
index 00000000..64b85b38
--- /dev/null
+++ b/min.lua
@@ -0,0 +1,16 @@
+vim.opt.rtp:append('~/workspace/dashboard-nvim')
+vim.opt.rtp:append('~/workspace/dashboard-doom')
+require('dashboard').setup()
+
+vim.opt.termguicolors = true
+vim.api.nvim_set_hl(0, 'DashboardHeader', {
+  fg = 'yellow',
+})
+
+vim.api.nvim_set_hl(0, 'DashboardCenter', {
+  fg = 'green',
+})
+
+vim.api.nvim_set_hl(0, 'DashboardFooter', {
+  fg = 'gray',
+})
diff --git a/plugin/dashboard.lua b/plugin/dashboard.lua
index a2d7d071..6f1420aa 100644
--- a/plugin/dashboard.lua
+++ b/plugin/dashboard.lua
@@ -1,19 +1,8 @@
--- version 0.2.3
+--version 0.3.0
 if vim.g.loaded_dashboard then
   return
 end
 
 vim.g.loaded_dashboard = 1
 
-vim.api.nvim_create_autocmd('UIEnter', {
-  group = vim.api.nvim_create_augroup('Dashboard', { clear = true }),
-  callback = function()
-    if vim.fn.argc() == 0 and vim.fn.line2byte('$') == -1 then
-      require('dashboard'):instance()
-    end
-  end,
-})
-
-vim.api.nvim_create_user_command('Dashboard', function()
-  require('dashboard'):instance()
-end, {})
+vim.api.nvim_create_user_command('Dashboard', function() end, {})
diff --git a/test/async_spec.lua b/test/async_spec.lua
new file mode 100644
index 00000000..0c757f9e
--- /dev/null
+++ b/test/async_spec.lua
@@ -0,0 +1,39 @@
+local afs = require('dashboard.asyncfs')
+local equal = assert.equal
+local path = './test/simple.txt'
+
+describe('asyncfs moudle', function()
+  setup(function()
+    os.execute('touch ./test/simple.txt')
+    local fd = io.open(path, 'w')
+    if fd then
+      fd:write('foo bar')
+      fd:close()
+    end
+  end)
+
+  teardown(function()
+    vim.fn.delete(path)
+  end)
+
+  it('can read file async', function()
+    local data
+    afs.async_read(path, function(content)
+      data = vim.split(content, '\n')
+    end)
+    vim.wait(1000)
+    equal('foo bar', data[1])
+  end)
+
+  it('can write file async', function()
+    afs.async_write(path, 'bar bar foo foo')
+    vim.wait(1000)
+    local fd = io.open(path, 'r')
+    if not fd then
+      print('open file failed in write callback')
+      return
+    end
+    local data = fd:read('*a')
+    equal('bar bar foo foo', data)
+  end)
+end)
diff --git a/test/util_spec.lua b/test/util_spec.lua
new file mode 100644
index 00000000..419a82bd
--- /dev/null
+++ b/test/util_spec.lua
@@ -0,0 +1,64 @@
+local util = require('dashboard.util')
+local eq = assert.equal
+local same = assert.same
+
+describe('util functions', function()
+  local bufnr
+  before_each(function()
+    bufnr = vim.api.nvim_create_buf(true, false)
+    vim.api.nvim_win_set_buf(0, bufnr)
+  end)
+
+  it('util.path_sep get path of system', function()
+    local iswin = vim.loop.os_uname().version:match('Windows')
+    local path = util.path_sep()
+    eq('/', path)
+    if iswin then
+      eq('\\', path)
+      vim.opt.shellslash = true
+      eq('/', path)
+    end
+  end)
+
+  it('util.path_join', function()
+    local nvim = '/Users/runner/.cache/nvim'
+    local dir = 'dashboard'
+    local path = util.path_join(nvim, dir)
+    eq('/Users/runner/.cache/nvim/dashboard', path)
+  end)
+
+  it('util.tail_align', function()
+    local lines = { 'balabala  ', 'foo  ', 'bar   ' }
+    lines = util.tail_align(lines)
+    same({
+      'balabala  ',
+      'foo       ',
+      'bar       ',
+    }, lines)
+  end)
+
+  it('util.center_align', function()
+    local lines = { 'balabala  ', 'foo  ', 'bar   ' }
+    lines = util.tail_align(lines)
+    lines = util.center_align(lines)
+    same({
+      '                                   balabala  ',
+      '                                   foo       ',
+      '                                   bar       ',
+    }, lines)
+  end)
+
+  it('util.generate_empty_table', function()
+    local tbl = util.generate_empty_table(2)
+    same({
+      '',
+      '',
+    }, tbl)
+  end)
+
+  it('util.get_vcs_root', function()
+    vim.api.nvim_buf_set_name(bufnr, 'test.lua')
+    local root = util.get_vcs_root(bufnr)
+    eq(vim.loop.cwd(), root)
+  end)
+end)