Skip to content

Commit 1263424

Browse files
Xiangnong WuXiangnong Wu
authored andcommitted
feat: initial release
1 parent edf2a3c commit 1263424

24 files changed

Lines changed: 4373 additions & 0 deletions

.github/workflows/docs.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: docs
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'README.md'
8+
- '.github/workflows/docs.yaml'
9+
10+
permissions:
11+
contents: write
12+
13+
jobs:
14+
panvimdoc:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Generate vimdoc from README
19+
uses: kdheepak/panvimdoc@main
20+
with:
21+
vimdoc: rubikscube
22+
version: "Neovim >= 0.10"
23+
pandoc: README.md
24+
toc: true
25+
treesitter: true
26+
demojify: false
27+
- name: Commit generated docs
28+
uses: stefanzweifel/git-auto-commit-action@v5
29+
with:
30+
commit_message: "docs: auto-generate vimdoc from README"
31+
file_pattern: doc/*.txt

.github/workflows/format.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: format
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
stylua:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: JohnnyMorganz/stylua-action@v4
14+
with:
15+
token: ${{ secrets.GITHUB_TOKEN }}
16+
version: latest
17+
args: --check .

.github/workflows/lint.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: lint
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
luacheck:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Install luacheck
14+
run: sudo apt-get update && sudo apt-get install -y luarocks && sudo luarocks install luacheck
15+
- name: Run luacheck
16+
run: luacheck lua/ tests/ scripts/ plugin/

.github/workflows/tests.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ${{ matrix.os }}
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
os: [ubuntu-latest, macos-latest]
15+
neovim: [stable, nightly]
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: rhysd/action-setup-vim@v1
19+
with:
20+
neovim: true
21+
version: ${{ matrix.neovim }}
22+
- name: Run tests
23+
run: nvim --headless -l tests/run.lua

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.DS_Store

.luacheckrc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
std = "luajit"
2+
cache = true
3+
codes = true
4+
5+
self = false
6+
7+
globals = {
8+
"vim",
9+
}
10+
11+
exclude_files = {
12+
"doc/",
13+
}
14+
15+
-- Tweak strictness for plugin idioms.
16+
ignore = {
17+
"212", -- unused argument
18+
"631", -- line too long
19+
}

README.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# rubiks-cube.nvim
2+
3+
A playable Rubik's cube inside Neovim — isometric ASCII render, full move set in Singmaster notation, a timer, and best-time persistence.
4+
5+
![rubiks-cub](assets/rubiks-cub.png)
6+
7+
## Requirements
8+
9+
- Neovim **0.10+** (uses `vim.uv`)
10+
- `termguicolors` enabled (recommended; falls back to cterm approximations otherwise)
11+
12+
## Installation
13+
14+
### lazy.nvim
15+
16+
```lua
17+
{
18+
"xiangnongWu2233/rubiks-cube.nvim",
19+
cmd = "Rubikscube",
20+
opts = {}, -- or pass any config table; see below
21+
}
22+
```
23+
24+
### packer.nvim
25+
26+
```lua
27+
use {
28+
"xiangnongWu2233/rubiks-cube.nvim",
29+
cmd = "Rubikscube",
30+
config = function() require("rubikscube").setup({}) end,
31+
}
32+
```
33+
34+
## Usage
35+
36+
```vim
37+
:Rubikscube
38+
```
39+
40+
## Features
41+
42+
- All 18 standard moves: face turns (`U D L R F B`) + cube rotations (`x y z`), each in CW/CCW form
43+
- Built-in timer with pause/resume on `<Space>`
44+
- Solve detection — flash + popup with time, move count, and personal best
45+
- Persistent best time stored at `stdpath("data")/rubikscube/best.json`
46+
- Floating window by default; configurable to open in current buffer, split, or vsplit
47+
- Fully configurable keymaps; any binding can be disabled with `false`
48+
49+
Inside the cube buffer:
50+
51+
| Key | Action |
52+
|---|---|
53+
| `u` / `U` | U face CW / CCW |
54+
| `d` / `D` | D face CW / CCW |
55+
| `l` / `L` | L face CW / CCW |
56+
| `r` / `R` | R face CW / CCW |
57+
| `f` / `F` | F face CW / CCW |
58+
| `b` / `B` | B face CW / CCW |
59+
| `x` / `X` | rotate whole cube around R axis |
60+
| `y` / `Y` | rotate whole cube around U axis |
61+
| `z` / `Z` | rotate whole cube around F axis |
62+
| `<Space>` | start / pause timer |
63+
| `s` | scramble (default 20 random moves) |
64+
| `<CR>` | reset to solved (clears timer) |
65+
| `?` | toggle help popup |
66+
| `q` | quit |
67+
68+
Lowercase = clockwise face turn (Singmaster notation); uppercase = prime (counter-clockwise).
69+
70+
## Configuration
71+
72+
Default:
73+
74+
```lua
75+
require("rubikscube").setup({
76+
keymaps = {
77+
-- Face turns: configure the lowercase letter; uppercase auto-binds the prime.
78+
U = "u", D = "d", L = "l", R = "r", F = "f", B = "b",
79+
-- Cube rotations: same convention.
80+
x = "x", y = "y", z = "z",
81+
-- Actions.
82+
scramble = "s",
83+
reset = "<CR>",
84+
timer = "<Space>",
85+
quit = "q",
86+
help = "?",
87+
-- Any entry above may be set to `false` to skip the binding entirely.
88+
},
89+
scramble_length = 20, -- moves applied by the scramble action
90+
persist_best = true, -- set false to disable writing best.json (reads still work)
91+
open_in = "float", -- "float" | "current" | "split" | "vsplit"
92+
})
93+
```
94+
95+
## Colors
96+
97+
Sticker colors are exposed as the highlight groups:
98+
99+
```
100+
RubiksW white
101+
RubiksY yellow
102+
RubiksO orange
103+
RubiksR red
104+
RubiksG green
105+
RubiksB blue
106+
```
107+
108+
Override any of them from your colorscheme — they're set with `default = true`, so user overrides survive a plugin reload:
109+
110+
```vim
111+
highlight RubiksO guibg=#ff8800 guifg=#000000
112+
```
113+
114+
## Commands
115+
116+
| Command | Description |
117+
|---|---|
118+
| `:Rubikscube` | Open the cube (no-op if a cube is already open) |

assets/rubiks-cub.png

1.25 MB
Loading

lua/rubikscube/best.lua

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
-- Persists the user's best solve time as JSON in stdpath("data").
2+
-- Schema: { time_ms = number, move_count = number, recorded_at = epoch_seconds }
3+
4+
local M = {}
5+
6+
-- Tests set this to redirect reads/writes away from the real data dir.
7+
M._path_override = nil
8+
9+
local function path()
10+
return M._path_override or (vim.fn.stdpath("data") .. "/rubikscube/best.json")
11+
end
12+
13+
local function ensure_parent_dir()
14+
-- pcall: degrade gracefully on read-only data dirs instead of aborting the solve flow.
15+
if M._path_override then
16+
local dir = vim.fn.fnamemodify(M._path_override, ":h")
17+
if dir ~= "" then
18+
pcall(vim.fn.mkdir, dir, "p")
19+
end
20+
else
21+
pcall(vim.fn.mkdir, vim.fn.stdpath("data") .. "/rubikscube", "p")
22+
end
23+
end
24+
25+
function M.read()
26+
local p = path()
27+
local fh = io.open(p, "r")
28+
if not fh then
29+
return nil
30+
end
31+
local s = fh:read("*a")
32+
fh:close()
33+
if not s or s == "" then
34+
return nil
35+
end
36+
local ok, data = pcall(vim.json.decode, s)
37+
if not ok or type(data) ~= "table" then
38+
return nil
39+
end
40+
return data
41+
end
42+
43+
function M.write(data)
44+
ensure_parent_dir()
45+
local fh, err = io.open(path(), "w")
46+
if not fh then
47+
return false, err
48+
end
49+
fh:write(vim.json.encode(data))
50+
fh:close()
51+
return true
52+
end
53+
54+
-- Returns the prior best time (ms) and whether `elapsed_ms` is a new best.
55+
-- Writes the new best if it improves on the prior. nil elapsed is a no-op.
56+
function M.maybe_update(elapsed_ms, move_count)
57+
if type(elapsed_ms) ~= "number" then
58+
return nil, false
59+
end
60+
local prior = M.read()
61+
local prior_ms = prior and prior.time_ms
62+
if prior_ms and prior_ms <= elapsed_ms then
63+
return prior_ms, false
64+
end
65+
M.write({
66+
time_ms = elapsed_ms,
67+
move_count = move_count,
68+
recorded_at = os.time(),
69+
})
70+
return prior_ms, true
71+
end
72+
73+
function M.clear()
74+
os.remove(path())
75+
end
76+
77+
return M

lua/rubikscube/config.lua

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
local M = {}
2+
3+
-- Face base letters: lowercase = clockwise, uppercase (auto) = prime.
4+
-- Rotation base letters: same lowercase/uppercase convention.
5+
-- Action keys: lhs strings passed to vim.keymap.set.
6+
-- Any keymap entry may be set to `false` to skip that binding entirely.
7+
M.defaults = {
8+
keymaps = {
9+
U = "u",
10+
D = "d",
11+
L = "l",
12+
R = "r",
13+
F = "f",
14+
B = "b",
15+
x = "x",
16+
y = "y",
17+
z = "z",
18+
scramble = "s",
19+
reset = "<CR>",
20+
timer = "<Space>",
21+
quit = "q",
22+
help = "?",
23+
},
24+
scramble_length = 20, -- moves applied by the scramble action
25+
persist_best = true, -- false → solves never write best.json (reads still work)
26+
open_in = "float", -- "float" | "current" | "split" | "vsplit"
27+
}
28+
29+
M.current = vim.deepcopy(M.defaults)
30+
31+
function M.apply(opts)
32+
if type(opts) ~= "table" then
33+
return
34+
end
35+
M.current = vim.tbl_deep_extend("force", M.current, opts)
36+
end
37+
38+
function M.reset()
39+
M.current = vim.deepcopy(M.defaults)
40+
end
41+
42+
function M.get()
43+
return M.current
44+
end
45+
46+
return M

0 commit comments

Comments
 (0)