diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8ac743b..71aa42b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,7 +43,6 @@ jobs: echo "$HOME/.local/bin" >> $GITHUB_PATH echo "$HOME/.local/share/mise/bin" >> $GITHUB_PATH echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH - mise use -g rust@1.92.0 - name: Install mise dependencies run: mise install diff --git a/.opencode/command/prose-code-diff.md b/.opencode/command/prose-code-diff.md index 7f5132a..90e8978 100644 --- a/.opencode/command/prose-code-diff.md +++ b/.opencode/command/prose-code-diff.md @@ -4,3 +4,5 @@ agent: build --- Use @prose-coder to review your CHANGE (not the rest of the code, JUST your change), and whittle it down (IF APPLICABLE) to a clean diff. While you are reviewing your change ONLY, review it in _context_ of whole (applicable) files. + +You can check what changed with either `git diff` or `git diff --staged`. diff --git a/.styluaignore b/.styluaignore index 905c617..dea6979 100644 --- a/.styluaignore +++ b/.styluaignore @@ -1 +1,2 @@ -lua/pm/morph +library +lua/tuis/morph diff --git a/AGENTS.md b/AGENTS.md index 4bbcc95..2e64744 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,21 @@ This document outlines the standard pattern for creating UI modules in the tuis.nvim plugin. +## Runtime + +Target LuaJIT in Neovim. DO NOT use goto, EVER. + + +## Build/Lint/Test Commands + +- `mise run ci` - Run lint, format check, and tests +- `mise run lint` - Typecheck with emmylua_check +- `mise run fmt` - Format code with stylua +- `mise run fmt:check` - Check code formatting +- `mise run test` - Run tests with busted + +**Single test**: Use busted directly: `busted --verbose --filter='""'` + ## UI Module Structure All UI modules should be located in `lua/tuis/uis/` and follow this pattern: @@ -88,6 +103,30 @@ To migrate an existing UI file: 5. Add `return M` at the end 6. Remove any early returns based on CLI availability (let `M.is_enabled()` handle this) + +### Testing Neovim Behaviors Interactively + +When you need to quickly test Neovim behaviors outside of the formal test suite: + +1. **Create a test script**: Create a standalone `.lua` file (e.g., `test_behavior.lua`) in the project root +2. **Write the test**: Use standard Lua and Neovim API calls. The script should end with `vim.cmd.qall { bang = true }` to exit +3. **Run the test**: Execute with `nvim --headless -u NONE -c "set rtp+=." -c "luafile test_behavior.lua" 2>&1` + - `--headless`: Run without UI + - `-u NONE`: Don't load user config + - `-c "set rtp+=."`: Add current directory to runtime path so `require 'morph'` works + - `-c "luafile test_behavior.lua"`: Execute your test script + - `2>&1`: Capture all output +4. **Clean up**: Delete test files when done (they should not be committed) + +**Example test script**: +```lua +#!/usr/bin/env nvim -l +-- Your test code here +vim.cmd.qall { bang = true } +``` + +**Note**: Interactive nvim commands with input (like `nvim --headless -c "..."` where commands expect user input) will hang. Always use non-interactive commands or scripts. + ## Examples See `lua/tuis/uis/launchd_services.lua` for a complete example of this pattern. diff --git a/lua/tuis/components.lua b/lua/tuis/components.lua index ce90638..d28e223 100644 --- a/lua/tuis/components.lua +++ b/lua/tuis/components.lua @@ -10,11 +10,9 @@ local function strdisplaywidth(s) return ok and w or #s end --- __ __ _ --- | \/ | ___| |_ ___ _ __ --- | |\/| |/ _ \ __/ _ \ '__| --- | | | | __/ || __/ | --- |_| |_|\___|\__\___|_| +-------------------------------------------------------------------------------- +-- Meter +-------------------------------------------------------------------------------- --- Unicode block characters for smooth horizontal progress display local METER_BLOCKS = { ' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█' } @@ -63,12 +61,9 @@ function M.Meter(ctx) return h('text', { hl = hl }, meter_str) end --- ____ _ _ _ --- / ___| _ __ __ _ _ __| | _| (_)_ __ ___ --- \___ \| '_ \ / _` | '__| |/ / | | '_ \ / _ \ --- ___) | |_) | (_| | | | <| | | | | | __/ --- |____/| .__/ \__,_|_| |_|\_\_|_|_| |_|\___| --- |_| +-------------------------------------------------------------------------------- +-- Sparkline +-------------------------------------------------------------------------------- --- @alias morph.SparklineProps { --- values: number[], @@ -118,11 +113,9 @@ function M.Sparkline(ctx) return h('text', { hl = hl }, sparkline_str) end --- _____ _ _ --- |_ _|_ _| |__ | | ___ --- | |/ _` | '_ \| |/ _ \ --- | | (_| | |_) | | __/ --- |_|\__,_|_.__/|_|\___| +-------------------------------------------------------------------------------- +-- Table +-------------------------------------------------------------------------------- --- @alias morph.TableBorderStyle 'none'|'single'|'double'|'rounded'|'ascii' @@ -426,11 +419,9 @@ function M.Table(ctx) return result end --- _____ _ ____ --- |_ _|_ _| |__ | __ ) __ _ _ __ --- | |/ _` | '_ \| _ \ / _` | '__| --- | | (_| | |_) | |_) | (_| | | --- |_|\__,_|_.__/|____/ \__,_|_| +-------------------------------------------------------------------------------- +-- TabBar +-------------------------------------------------------------------------------- --- @class morph.TabBarTab --- @field key string @@ -486,12 +477,9 @@ function M.TabBar(ctx) return { result, '\n\n' } end --- _ _ _ --- | | | | ___| |_ __ --- | |_| |/ _ \ | '_ \ --- | _ | __/ | |_) | --- |_| |_|\___|_| .__/ --- |_| +-------------------------------------------------------------------------------- +-- Help +-------------------------------------------------------------------------------- --- @alias morph.HelpKeymap { [1]: string, [2]: string } diff --git a/lua/tuis/term_emulator.lua b/lua/tuis/term_emulator.lua index 2d85a79..a410c38 100644 --- a/lua/tuis/term_emulator.lua +++ b/lua/tuis/term_emulator.lua @@ -32,7 +32,7 @@ local function detect_terminal() end end ---- @class TerminalInfo +--- @class tuis.term_emulator.TerminalInfo --- @field terminal string The detected terminal type ('tmux', 'wezterm', 'ghostty', 'unknown') --- @field is_wsl2 boolean Whether running in WSL2 --- @field is_tmux boolean Whether running in TMUX @@ -40,7 +40,7 @@ end --- @field is_ghostty boolean Whether running in Ghostty --- @field wezterm_path string Path to WezTerm executable ---- @class Emulator +--- @class tuis.term_emulator.Emulator --- @field name string local Emulator = {} M.Emulator = Emulator @@ -48,7 +48,7 @@ Emulator.__index = Emulator --- Get emulator instance by kind, or currently detected if kind is nil --- @param kind string? ---- @return Emulator? +--- @return tuis.term_emulator.Emulator? function Emulator.get(kind) kind = kind or detect_terminal() @@ -64,7 +64,7 @@ function Emulator.get(kind) end --- @param name string ---- @return Emulator +--- @return tuis.term_emulator.Emulator function Emulator:new(name) return setmetatable({ name = name }, self) end --- @param _program string? @@ -93,7 +93,7 @@ end -- WezTerm -------------------------------------------------------------------------------- ---- @class WezTerm : Emulator +--- @class tuis.term_emulator.WezTerm : tuis.term_emulator.Emulator local WezTerm = setmetatable({}, { __index = Emulator }) M.WezTerm = WezTerm WezTerm.__index = WezTerm @@ -107,7 +107,7 @@ function WezTerm.get_wezterm_path() end end ---- @return WezTerm +--- @return tuis.term_emulator.WezTerm function WezTerm:new() return setmetatable(Emulator.new(self, 'wezterm'), self) end --- @param program string? @@ -174,12 +174,12 @@ end -- Tmux -------------------------------------------------------------------------------- ---- @class Tmux : Emulator +--- @class tuis.term_emulator.Tmux : tuis.term_emulator.Emulator local Tmux = setmetatable({}, { __index = Emulator }) M.Tmux = Tmux Tmux.__index = Tmux ---- @return Tmux +--- @return tuis.term_emulator.Tmux function Tmux:new() return setmetatable(Emulator.new(self, 'tmux'), self) end --- @param program string? @@ -213,12 +213,12 @@ end -- Ghostty -------------------------------------------------------------------------------- ---- @class Ghostty : Emulator +--- @class tuis.term_emulator.Ghostty : tuis.term_emulator.Emulator local Ghostty = setmetatable({}, { __index = Emulator }) M.Ghostty = Ghostty Ghostty.__index = Ghostty ---- @return Ghostty +--- @return tuis.term_emulator.Ghostty function Ghostty:new() return setmetatable(Emulator.new(self, 'ghostty'), self) end --- @param program string? @@ -326,7 +326,7 @@ function M.new_tab(program, emulator) end --- Get the currently detected emulator instance ---- @return Emulator? +--- @return tuis.term_emulator.Emulator? function M.current() return Emulator.get() end return M diff --git a/lua/tuis/uis/aws.lua b/lua/tuis/uis/aws.lua index 3b93dde..f55b112 100644 --- a/lua/tuis/uis/aws.lua +++ b/lua/tuis/uis/aws.lua @@ -551,8 +551,9 @@ local function App(ctx) state.loading = true ctx:update(state) + local page = assert(state.page) --- @type aws.PageConfig - local config = PAGE_CONFIGS[state.page] + local config = assert(PAGE_CONFIGS[page]) config.fetch(state.region, function(items) state[config.state_key] = items state.loading = false @@ -600,10 +601,10 @@ local function App(ctx) end -- Render current page - --- @cast state.page aws.Page - local page = state.page + local page = assert(state.page) + --- @cast page aws.Page --- @type aws.PageConfig - local config = PAGE_CONFIGS[page] + local config = assert(PAGE_CONFIGS[page]) local page_content = h(config.view, { items = state[config.state_key], loading = state.loading, diff --git a/lua/tuis/uis/bitwarden.lua b/lua/tuis/uis/bitwarden.lua index 2dce33e..04bdab8 100644 --- a/lua/tuis/uis/bitwarden.lua +++ b/lua/tuis/uis/bitwarden.lua @@ -1022,8 +1022,8 @@ local function App(ctx) local state = assert(ctx.state) if ctx.phase == 'unmount' then - state.timer:stop() - state.timer:close() + assert(state.timer):stop() + assert(state.timer):close() end -- Build navigation keymaps @@ -1072,7 +1072,7 @@ local function App(ctx) folders = state.folders, items = state.items, loading = state.loading, - on_filter_folder = function(folder_id) + on_filter_folder = function(_folder_id) -- Switch to items page with folder filter state.page = 'items' ctx:update(state) diff --git a/lua/tuis/uis/docker.lua b/lua/tuis/uis/docker.lua index 0905826..f6149e4 100644 --- a/lua/tuis/uis/docker.lua +++ b/lua/tuis/uis/docker.lua @@ -147,9 +147,9 @@ local function show_inspect(type, id) end --- Ask for confirmation before running a dangerous action ---- @param prompt string +--- @param _prompt string --- @param on_confirm fun() -local function confirm_action(prompt, on_confirm) +local function confirm_action(_prompt, on_confirm) local choice = vim.fn.confirm('Are you sure?', '&Yes\n&No', 2) if choice == 1 then vim.schedule(on_confirm) end end @@ -1015,7 +1015,7 @@ local StatsView = create_resource_view { } end, - keymaps = function(stat, on_refresh) + keymaps = function(stat, _on_refresh) return { ['gi'] = keymap(function() show_inspect('container', stat.id) end), ['gl'] = keymap(function() term.open('docker logs --since 5m -f ' .. stat.id) end), @@ -1046,7 +1046,7 @@ local ComposeView = create_resource_view { } end, - keymaps = function(project, on_refresh) + keymaps = function(project, _on_refresh) return { ['gi'] = keymap(function() vim.schedule(function() @@ -1516,7 +1516,8 @@ local function App(ctx) ctx:update(state) end - local config = PAGE_CONFIGS[state.page] + local page = assert(state.page) + local config = assert(PAGE_CONFIGS[page]) if config.custom and state.page == 'system' then -- System page returns two values: disk_usage and info config.fetch(function(disk_usage, info) @@ -1525,7 +1526,7 @@ local function App(ctx) state.loading = false ctx:update(state) end) - elseif state.page == 'contexts' then + elseif page == 'contexts' then -- Contexts page also updates header context display config.fetch(function(items) state[config.state_key] = items @@ -1533,7 +1534,7 @@ local function App(ctx) state.loading = false ctx:update(state) end) - elseif state.page == 'hub' then + elseif page == 'hub' then state.loading = false ctx:update(state) else @@ -1591,10 +1592,9 @@ local function App(ctx) end local state = assert(ctx.state) - if ctx.phase == 'unmount' then - state.timer:stop() - state.timer:close() + assert(state.timer):stop() + assert(state.timer):close() end -- Build navigation keymaps @@ -1614,9 +1614,10 @@ local function App(ctx) end -- Render current page - local config = PAGE_CONFIGS[state.page] + local page = assert(state.page) + local config = assert(PAGE_CONFIGS[page]) local page_content - if config.custom and state.page == 'system' then + if config.custom and page == 'system' then page_content = h(config.view, { disk_usage = state.disk_usage, info = state.system_info, diff --git a/lua/tuis/uis/gcloud.lua b/lua/tuis/uis/gcloud.lua index 224d22a..b3a2669 100644 --- a/lua/tuis/uis/gcloud.lua +++ b/lua/tuis/uis/gcloud.lua @@ -548,8 +548,9 @@ local function App(ctx) state.loading = true ctx:update(state) + local page = assert(state.page) --- @type gcloud.PageConfig - local config = PAGE_CONFIGS[state.page] + local config = assert(PAGE_CONFIGS[page]) config.fetch(state.project, function(items) state[config.state_key] = items state.loading = false @@ -597,10 +598,10 @@ local function App(ctx) end -- Render current page - --- @cast state.page gcloud.Page - local page = state.page + local page = assert(state.page) + --- @cast page gcloud.Page --- @type gcloud.PageConfig - local config = PAGE_CONFIGS[page] + local config = assert(PAGE_CONFIGS[page]) local page_content = h(config.view, { items = state[config.state_key], loading = state.loading, diff --git a/lua/tuis/uis/github.lua b/lua/tuis/uis/github.lua index 67b3985..37eb258 100644 --- a/lua/tuis/uis/github.lua +++ b/lua/tuis/uis/github.lua @@ -6,7 +6,6 @@ local TabBar = components.TabBar local Help = components.Help local term = require 'tuis.term' local utils = require 'tuis.utils' -local keymap = utils.keymap local M = {} @@ -649,9 +648,9 @@ local HELP_KEYMAPS = { } --- Help component ---- @param ctx morph.Ctx<{}> +--- @param _ctx morph.Ctx --- @return morph.Tree[] -local function GithubHelp(ctx) return h(Help, { common_keymaps = HELP_KEYMAPS }) end +local function GithubHelp(_ctx) return h(Help, { common_keymaps = HELP_KEYMAPS }) end --- Breadcrumb component for detail views --- @param ctx morph.Ctx<{ page: gh.Page, repo: string|nil, selected_pr: number|nil, selected_issue: number|nil, selected_run: number|nil, on_back: fun() }> @@ -1420,7 +1419,8 @@ local function App(ctx) return end - local config = PAGE_CONFIG[state.page] + local page = assert(state.page) + local config = assert(PAGE_CONFIG[page]) if not config then return end local has_data = state[config.field] @@ -1519,7 +1519,8 @@ local function App(ctx) end -- Render page content using config - local config = PAGE_CONFIG[state.page] + local page = assert(state.page) + local config = assert(PAGE_CONFIG[page]) local page_content = config and h( config.component, diff --git a/lua/tuis/uis/hacker_news.lua b/lua/tuis/uis/hacker_news.lua index f059aff..ba104c6 100644 --- a/lua/tuis/uis/hacker_news.lua +++ b/lua/tuis/uis/hacker_news.lua @@ -461,7 +461,7 @@ local function CommentsView(ctx) if not is_collapsed then local text = strip_html(c.text) local wrapped = wrap_text(text, content_width) - for i, line in ipairs(wrapped) do + for _i, line in ipairs(wrapped) do table.insert(rendered_lines, h('text', {}, indent .. line)) end end diff --git a/lua/tuis/uis/k8s.lua b/lua/tuis/uis/k8s.lua index 8e3cb62..73ff97f 100644 --- a/lua/tuis/uis/k8s.lua +++ b/lua/tuis/uis/k8s.lua @@ -902,8 +902,9 @@ local function App(ctx) ctx:update(state) end + local page = assert(state.page) --- @type k8s.PageConfig - local config = PAGE_CONFIGS[state.page] + local config = assert(PAGE_CONFIGS[page]) local function on_data(items) state[config.state_key] = items state.loading = false @@ -953,8 +954,8 @@ local function App(ctx) -- Cleanup timer on unmount if ctx.phase == 'unmount' then - state.timer:stop() - state.timer:close() + assert(state.timer):stop() + assert(state.timer):close() end -- Build navigation keymaps programmatically @@ -974,10 +975,8 @@ local function App(ctx) end -- Render current page - --- @cast state.page k8s.Page - local page = state.page --- @type k8s.PageConfig - local config = PAGE_CONFIGS[page] + local config = assert(PAGE_CONFIGS[state.page]) local page_content = h(config.view, { items = state[config.state_key], loading = state.loading, diff --git a/lua/tuis/uis/launchd.lua b/lua/tuis/uis/launchd.lua index f2754c4..db67533 100644 --- a/lua/tuis/uis/launchd.lua +++ b/lua/tuis/uis/launchd.lua @@ -95,8 +95,8 @@ local TABS = { local HELP_KEYMAPS = { service = { { 'gi', 'Show service details' }, - { 'gs', 'Start service (load)' }, - { 'gS', 'Stop service (bootout)' }, + { 'gs', 'Start service' }, + { 'gS', 'Stop service' }, }, agent = { { 'gi', 'Show agent plist' }, @@ -240,7 +240,7 @@ local function fetch_agents(namespace, callback) end --- @param callback fun(daemons: morphui.launchd.DaemonInfo[]) -local function fetch_daemons(namespace, callback) +local function fetch_daemons(_namespace, callback) local base_dir = '/Library/LaunchDaemons/' vim.system( @@ -312,7 +312,7 @@ local function fetch_daemons(namespace, callback) end --- @param callback fun(limits: morphui.launchd.LimitInfo[]) -local function fetch_limits(namespace, callback) +local function fetch_limits(_namespace, callback) run( { 'launchctl', 'limit' }, { text = true }, @@ -425,7 +425,7 @@ local ServicesView = create_resource_view { ['gs'] = keymap( function() run( - { 'launchctl', 'load', service.name }, + { 'launchctl', 'start', service.name }, { root = namespace == 'system', text = true }, vim.schedule_wrap(on_refresh) ) @@ -433,11 +433,11 @@ local ServicesView = create_resource_view { ), ['gS'] = keymap( function() - run({ - 'launchctl', - 'bootout', - (namespace == 'system' and 'system/' or '') .. service.name, - }, { root = namespace == 'system', text = true }, vim.schedule_wrap(on_refresh)) + run( + { 'launchctl', 'stop', service.name }, + { root = namespace == 'system', text = true }, + vim.schedule_wrap(on_refresh) + ) end ), } @@ -461,7 +461,7 @@ local AgentsView = create_resource_view { } end, - keymaps = function(agent, on_refresh, namespace) + keymaps = function(agent, on_refresh, _namespace) return { ['gi'] = keymap(function() vim.cmd.vnew() @@ -511,7 +511,7 @@ local DaemonsView = create_resource_view { } end, - keymaps = function(daemon, on_refresh, namespace) + keymaps = function(daemon, on_refresh, _namespace) return { ['gi'] = keymap(function() vim.cmd.vnew() @@ -649,7 +649,8 @@ local function App(ctx) ctx:update(state) end - local config = PAGE_CONFIGS[state.page] + local page = assert(state.page) + local config = assert(PAGE_CONFIGS[page]) config.fetch(state.namespace, function(items) state[config.state_key] = items state.loading = false @@ -709,7 +710,8 @@ local function App(ctx) end) end - local config = PAGE_CONFIGS[state.page] + local page = assert(state.page) + local config = assert(PAGE_CONFIGS[page]) local current_items = state[config.state_key] or {} local page_view = h(config.view, { diff --git a/lua/tuis/uis/lsof.lua b/lua/tuis/uis/lsof.lua index eb006ee..1b53278 100644 --- a/lua/tuis/uis/lsof.lua +++ b/lua/tuis/uis/lsof.lua @@ -252,8 +252,8 @@ local function App(ctx) local state = assert(ctx.state) if ctx.phase == 'unmount' then - state.timer:stop() - state.timer:close() + assert(state.timer):stop() + assert(state.timer):close() end return h('text', { diff --git a/lua/tuis/uis/models-dev.lua b/lua/tuis/uis/models-dev.lua index f47e7b7..66a977f 100644 --- a/lua/tuis/uis/models-dev.lua +++ b/lua/tuis/uis/models-dev.lua @@ -172,9 +172,9 @@ local COMMON_KEYMAPS = { { 'g?', 'Toggle help' }, } ---- @param ctx morph.Ctx<{}> +--- @param _ctx morph.Ctx --- @return morph.Tree[] -local function ModelsHelp(ctx) +local function ModelsHelp(_ctx) return { h(Help, { page_keymaps = PAGE_KEYMAPS, common_keymaps = COMMON_KEYMAPS }), '\n', diff --git a/lua/tuis/uis/plugin_store.lua b/lua/tuis/uis/plugin_store.lua index 8585554..ae5f691 100644 --- a/lua/tuis/uis/plugin_store.lua +++ b/lua/tuis/uis/plugin_store.lua @@ -1,11 +1,9 @@ local Morph = require 'tuis.morph' local h = Morph.h local components = require 'tuis.components' -local Table = components.Table local TabBar = components.TabBar local Help = components.Help local utils = require 'tuis.utils' -local keymap = utils.keymap local M = {} @@ -170,7 +168,7 @@ local function format_relative_time(iso_date) if seconds_ago < 0 then return 'just now' end -- Time thresholds in seconds - local MINUTE, HOUR, DAY, WEEK, MONTH, YEAR = 60, 3600, 86400, 604800, 2592000, 31536000 + local DAY, WEEK, MONTH, YEAR = 86400, 604800, 2592000, 31536000 if seconds_ago < DAY then return 'today' end if seconds_ago < WEEK then return math.floor(seconds_ago / DAY) .. 'd ago' end @@ -691,8 +689,8 @@ local HELP_KEYMAPS = { { 'g?', 'Toggle help' }, } ---- @param ctx morph.Ctx<{}> -local function PluginStoreHelp(ctx) return h(Help, { common_keymaps = HELP_KEYMAPS }) end +--- @param _ctx morph.Ctx +local function PluginStoreHelp(_ctx) return h(Help, { common_keymaps = HELP_KEYMAPS }) end -------------------------------------------------------------------------------- -- UI Components: Navigation Tabs @@ -748,13 +746,8 @@ end --- @param ctx morph.Ctx<{ categories: ps.Category[], installed: table, loading: boolean, on_select: fun(p: ps.Plugin), on_install: fun(p: ps.Plugin), on_update: fun(p: ps.Plugin), on_remove: fun(p: ps.Plugin), on_toggle_category: fun(slug: string) }, ps.BrowseState> local function BrowseView(ctx) -- Initialize state on mount, or ensure defaults exist for re-renders - if ctx.phase == 'mount' then - ctx.state = { filter = '' } - else - ctx.state = ctx.state or {} - ctx.state.filter = ctx.state.filter or '' - end - local state = ctx.state + if ctx.phase == 'mount' then ctx.state = { filter = '' } end + local state = assert(ctx.state) local filter_lower = state.filter:lower() -- Header @@ -1000,7 +993,7 @@ local function InstalledView(ctx) end end - local state = ctx.state + local state = assert(ctx.state) local content = { h.RenderMarkdownH1({}, '# Plugin Store - Installed'), diff --git a/lua/tuis/uis/processes.lua b/lua/tuis/uis/processes.lua index 9784e5f..6143a1b 100644 --- a/lua/tuis/uis/processes.lua +++ b/lua/tuis/uis/processes.lua @@ -7,11 +7,10 @@ local Sparkline = components.Sparkline local TabBar = components.TabBar local Help = components.Help local utils = require 'tuis.utils' -local keymap = utils.keymap local M = {} -local platform = vim.loop.os_uname().sysname +local platform = vim.uv.os_uname().sysname local is_macos = platform == 'Darwin' local is_linux = platform == 'Linux' @@ -107,40 +106,67 @@ local function fetch_cpu_stats(callback) end) end) elseif is_linux then - local file = io.open('/proc/stat', 'r') - if not file then - callback { overall = 0, cores = {} } - return + local function read_cpu_stats() + local file = io.open('/proc/stat', 'r') + local content = file:read '*a' + file:close() + + -- The /proc/stat file is arranged in `LABEL VALUE [VALUE...]` lines + --- @type table + local proc_stat = vim.tbl_extend( + 'force', + unpack(vim + .iter(vim.split(content, '\n')) + :filter(function(line) return vim.trim(line) ~= '' end) + :map(function(line) + local values = vim.iter(line:gmatch '%S+'):totable() + local label = table.remove(values, 1) + if not vim.startswith(label, 'cpu') then return {} end + return { + -- convert each value to a number: + [label] = vim.iter(values):map(function(v) return tonumber(v) end):totable(), + } + end) + :totable()) + ) + + return proc_stat end - local content = file:read '*a' - file:close() + local stats1 = read_cpu_stats() + vim.defer_fn(function() + local stats2 = read_cpu_stats() + + --- @return number + local function calculate_usage(v1, v2) + assert(#v1 > 4, 'v1 should have at least 4 elements') + assert(#v2 > 4, 'v2 should have at least 4 elements') + assert(#v1 == #v2, 'v1 should have the same number of elements as v2') + + local idle1, idle2 = v1[4], v2[4] + local total1, total2 = 0, 0 + for i = 1, #v1 do + total1 = total1 + v1[i] + total2 = total2 + v2[i] + end + local total_delta = total2 - total1 + local idle_delta = idle2 - idle1 + if total_delta <= 0 then return 0 end + return ((total_delta - idle_delta) / total_delta) * 100 + end - local user, nice, system, idle = content:match 'cpu%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)' - if not user then - callback { overall = 0, cores = {} } - return - end + local overall = calculate_usage(stats1['cpu'], stats2['cpu']) - local total = (tonumber(user) or 0) - + (tonumber(nice) or 0) - + (tonumber(system) or 0) - + (tonumber(idle) or 0) - local overall = ((total - (tonumber(idle) or 0)) / total) * 100 - - local cores = {} - for core_user, core_nice, core_sys, core_idle in - content:gmatch 'cpu%d+%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)' - do - local core_total = (tonumber(core_user) or 0) - + (tonumber(core_nice) or 0) - + (tonumber(core_sys) or 0) - + (tonumber(core_idle) or 0) - local core_usage = ((core_total - (tonumber(core_idle) or 0)) / core_total) * 100 - table.insert(cores, core_usage) - end + local cores = {} + for _, key in ipairs(vim.tbl_keys(stats1)) do + if key ~= 'cpu' then + local usage = calculate_usage(stats1[key], stats2[key]) + table.insert(cores, usage) + end + end - callback { overall = overall, cores = cores } + callback { overall = overall, cores = cores } + end, 200) else callback { overall = 0, cores = {} } end @@ -591,7 +617,7 @@ local function get_disk_usage(callback) local filesystem = parts[1] local size = parts[2] local used = parts[3] - local avail = parts[4] + -- local _avail = parts[4] local percent_col = parts[5] local mounted diff --git a/lua/tuis/uis/systemd.lua b/lua/tuis/uis/systemd.lua index e389355..bfa94e5 100644 --- a/lua/tuis/uis/systemd.lua +++ b/lua/tuis/uis/systemd.lua @@ -647,7 +647,7 @@ local SlicesView = create_resource_view { } end, - keymaps = function(unit, on_refresh, namespace) + keymaps = function(unit, _on_refresh, namespace) return { ['gi'] = keymap(function() show_inspect(namespace, unit.unit) end), } @@ -713,7 +713,7 @@ local DevicesView = create_resource_view { } end, - keymaps = function(unit, on_refresh, namespace) + keymaps = function(unit, _on_refresh, namespace) return { ['gi'] = keymap(function() show_inspect(namespace, unit.unit) end), } @@ -786,7 +786,7 @@ local function App(ctx) ctx:update(state) end - local config = PAGE_CONFIGS[state.page] + local config = assert(PAGE_CONFIGS[state.page]) local fetch_fn = state.namespace == 'user' and function(cb) @@ -868,7 +868,8 @@ local function App(ctx) end) end - local config = PAGE_CONFIGS[state.page] + local page = assert(state.page) + local config = assert(PAGE_CONFIGS[page]) local current_items = state[config.state_key] or {} local page_view = h(config.view, { diff --git a/mise.toml b/mise.toml index 2bd4e0a..b0d549c 100644 --- a/mise.toml +++ b/mise.toml @@ -1,26 +1,50 @@ +################################################################################ +## Tool Alias +################################################################################ + +[tool_alias] +gh_emmylua_ls = "github:EmmyLuaLs/emmylua-analyzer-rust" +gh_emmylua_check = "github:EmmyLuaLs/emmylua-analyzer-rust" + +################################################################################ +## Tools +################################################################################ + [tools] lua = "5.1.5" neovim = "0.11.5" stylua = "2.3.1" -[tools."cargo:emmylua_check"] +[tools.gh_emmylua_ls] version = "0.19.0" -url = "https://github.com/EmmyLuaLs/emmylua-analyzer-rust@tag:0.19.0" -crate = "emmylua_check" +[tools.gh_emmylua_ls.platforms] +linux-x64 = { asset_pattern = "*_ls-linux-x64*" } +macos-x64 = { asset_pattern = "*_ls-darwin-x64*" } +macos-arm64 = { asset_pattern = "*_ls-darwin-arm64*" } -[tools."cargo:emmylua_ls"] +[tools.gh_emmylua_check] version = "0.19.0" -url = "https://github.com/EmmyLuaLs/emmylua-analyzer-rust@tag:0.19.0" -crate = "emmylua_ls" +[tools.gh_emmylua_check.platforms] +linux-x64 = { asset_pattern = "*_check-linux-x64*" } +macos-x64 = { asset_pattern = "*_check-darwin-x64*" } +macos-arm64 = { asset_pattern = "*_check-darwin-arm64*" } + +################################################################################ +# Env +################################################################################ [env] ASDF_LUA_LUAROCKS_VERSION = "3.12.2" _.source = { path = "./scripts/env.sh", tools = true } +################################################################################ +# Tasks +################################################################################ + [tasks] lint = "emmylua_check ." -fmt = "stylua ." -"fmt:check" = "stylua --check ." +fmt = "stylua --verbose ." +"fmt:check" = "stylua --verbose --check ." [tasks."test"] hide = true @@ -34,5 +58,6 @@ busted --verbose [tasks."ci"] run = ''' mise run fmt:check +mise run lint mise run test ''' diff --git a/spec/components_spec.lua b/spec/components_spec.lua index 0d20176..f10dcc9 100644 --- a/spec/components_spec.lua +++ b/spec/components_spec.lua @@ -15,13 +15,11 @@ local function with_buf(lines, f) vim.cmd.bdelete { bang = true } if not ok then error(result) end end - describe('Morph-UI Components', function() - -- __ __ _ - -- | \/ | ___| |_ ___ _ __ - -- | |\/| |/ _ \ __/ _ \ '__| - -- | | | | __/ || __/ | - -- |_| |_|\___|\__\___|_| + -------------------------------------------------------------------------------- + -- Meter + -------------------------------------------------------------------------------- + describe('Meter', function() it('should render an empty meter when value is 0', function() with_buf({}, function() @@ -158,12 +156,10 @@ describe('Morph-UI Components', function() end) end) - -- ____ _ _ _ - -- / ___| _ __ __ _ _ __| | _| (_)_ __ ___ - -- \___ \| '_ \ / _` | '__| |/ / | | '_ \ / _ \ - -- ___) | |_) | (_| | | | <| | | | | | __/ - -- |____/| .__/ \__,_|_| |_|\_\_|_|_| |_|\___| - -- |_| + -------------------------------------------------------------------------------- + -- Sparkline + -------------------------------------------------------------------------------- + describe('Sparkline', function() it('should render empty sparkline for empty values', function() with_buf({}, function() @@ -290,11 +286,10 @@ describe('Morph-UI Components', function() end) end) - -- _____ _ _ - -- |_ _|_ _| |__ | | ___ - -- | |/ _` | '_ \| |/ _ \ - -- | | (_| | |_) | | __/ - -- |_|\__,_|_.__/|_|\___| + -------------------------------------------------------------------------------- + -- Table + -------------------------------------------------------------------------------- + describe('Table', function() it('should render a simple table with aligned columns', function() with_buf({}, function() @@ -1040,10 +1035,10 @@ describe('Morph-UI Components', function() end) end) - -- _____ _ _ _ _ __ _ - -- |_ _|_ _| |__ | | ___ | | | |/ _` | - -- | |/ _` | '_ \| |/ _ \ | | | | (_| | - -- |_|\__,_|_.__/|_|\___| |_| |_|\__,_| + -------------------------------------------------------------------------------- + -- TabBar + -------------------------------------------------------------------------------- + describe('TabBar', function() local tabs = { { key = 'g1', page = 'containers', label = 'Containers' }, @@ -1078,9 +1073,6 @@ describe('Morph-UI Components', function() local r = Morph.new(0) r:mount(h(TabBar, { tabs = tabs, active_page = 'images' })) - local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) - local text = table.concat(lines, '\n') - -- Active tab should be highlighted with H2Bg -- The inactive tabs use H2, active uses H2Bg local ns =