From ed828ab4f61291f42c5acd10e65f33ef753681b9 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Mon, 26 Jan 2026 10:01:31 +0900 Subject: [PATCH 01/19] chore: add permissions for write access in GitHub Actions workflow --- .github/workflows/publish.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index f2c9e85..2bef074 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -6,6 +6,9 @@ on: - main types: [closed] +permissions: + contents: write + jobs: setup: runs-on: ubuntu-latest From 067c2b03750a95d1dc9032379ea1e9cb65a285c0 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Mon, 26 Jan 2026 16:13:45 +0900 Subject: [PATCH 02/19] fix: reorder badge display in README for better visibility. --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8c6c4b8..a58e680 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ # sibling -[![build](https://github.com/tamada/sibling/actions/workflows/build.yaml/badge.svg)](https://github.com/tamada/sibling/actions/workflows/build.yaml) -[![Coverage Status](https://coveralls.io/repos/github/tamada/sibling/badge.svg?branch=main)](https://coveralls.io/github/tamada/sibling?branch=main) - -[![Rust Report Card](https://rust-reportcard.xuri.me/badge/github.com/tamada/sibling)](https://rust-reportcard.xuri.me/report/github.com/tamada/sibling) - [![crates.io](https://img.shields.io/badge/crates.io-v2.0.4-orange.svg?logo=rust)](https://crates.io/crates/sibling) [![License](https://img.shields.io/badge/License-WTFPL-information.svg)](https://github.com/tamada/sibling/blob/master/LICENSE) [![Version](https://img.shields.io/badge/Version-v2.0.4-information.svg)](https://github.com/tamada/sibling/releases/tag/v2.0.4) +[![build](https://github.com/tamada/sibling/actions/workflows/build.yaml/badge.svg)](https://github.com/tamada/sibling/actions/workflows/build.yaml) +[![Coverage Status](https://coveralls.io/repos/github/tamada/sibling/badge.svg?branch=main)](https://coveralls.io/github/tamada/sibling?branch=main) +[![Rust Report Card](https://rust-reportcard.xuri.me/badge/github.com/tamada/sibling)](https://rust-reportcard.xuri.me/report/github.com/tamada/sibling) + get the next/previous sibling directory name. ## :speaking_head: Description From c6fdc851e865e53ebd1b4de376d1926bca341fac Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 26 Jan 2026 07:14:02 +0000 Subject: [PATCH 03/19] update version to 2.0.5, ready to publish v2.0.5 --- Cargo.toml | 2 +- README.md | 4 ++-- docs/config.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0142b2a..bbe319e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [ "lib", "cli" ] resolver = "3" [workspace.package] -version = "2.0.4" +version = "2.0.5" repository = "https://github.com/tamada/sibling" homepage = "https://tamada.github.io/sibling" readme = "README.md" diff --git a/README.md b/README.md index a58e680..ec1e7da 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # sibling -[![crates.io](https://img.shields.io/badge/crates.io-v2.0.4-orange.svg?logo=rust)](https://crates.io/crates/sibling) +[![crates.io](https://img.shields.io/badge/crates.io-v2.0.5-orange.svg?logo=rust)](https://crates.io/crates/sibling) [![License](https://img.shields.io/badge/License-WTFPL-information.svg)](https://github.com/tamada/sibling/blob/master/LICENSE) -[![Version](https://img.shields.io/badge/Version-v2.0.4-information.svg)](https://github.com/tamada/sibling/releases/tag/v2.0.4) +[![Version](https://img.shields.io/badge/Version-v2.0.5-information.svg)](https://github.com/tamada/sibling/releases/tag/v2.0.5) [![build](https://github.com/tamada/sibling/actions/workflows/build.yaml/badge.svg)](https://github.com/tamada/sibling/actions/workflows/build.yaml) [![Coverage Status](https://coveralls.io/repos/github/tamada/sibling/badge.svg?branch=main)](https://coveralls.io/github/tamada/sibling?branch=main) diff --git a/docs/config.toml b/docs/config.toml index 1f1f0fe..0601028 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -15,7 +15,7 @@ pygmentsStyle = "pygments" project_tagline = "get next/previous sibling directory name" dateFormat = "2006-01-02" katex = true - version = "2.0.4" + version = "2.0.5" footer = "[![GitHub](https://img.shields.io/badge/GitHub-tamada/sibling-blueviolet.svg?logo=github)](https://github.com/tamada/sibling) Made with [Hugo](https://gohugo.io/). Theme by [Cayman](https://github.com/zwbetz-gh/cayman-hugo-theme). Deployed to [GitHub Pages](https://pages.github.com/)." [menu] From ad9b45ea69b67a07c589753fc792f6cfd87df0e6 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Sat, 7 Feb 2026 13:37:45 +0900 Subject: [PATCH 04/19] add logo --- docs/resources/logo.pptx | Bin 0 -> 67301 bytes docs/static/images/logo.svg | 1 + 2 files changed, 1 insertion(+) create mode 100644 docs/resources/logo.pptx create mode 100644 docs/static/images/logo.svg diff --git a/docs/resources/logo.pptx b/docs/resources/logo.pptx new file mode 100644 index 0000000000000000000000000000000000000000..7e5e45007965fe3e8730eeb32b0c91efcdae0545 GIT binary patch literal 67301 zcmeFYW0Yml(k*z>wr$&$&aAX;+eW2r+qP|0s?xS?J1gCJ?|uFHd;Q%WxBtJlf1I<| z-fN9^8mo#^Z(EFzp(`-QrBgHnGi#+WxPUV?BKKnL|AFH-P-O1i1`FI z@$%MeGFu3{x*IzM;%Fhj4DHx0%szd4Br;pp&!iQRqWaWpjD;-?l0gZLlI=OeMzpT; z=Sclsq{*czp^(x8$g-bKB`P@<^+A-SaN_fwUPBad5Qt3Jm9a@hU&o3jc4uJAOm8W0 z0M%-haM(1b0v!?KJmQ@$5Z>&_^liC2&VqOO1_A-=#7vcFNI9n9?tUqjO|+@T_lM9R z{WaogO98_Z23avC4RcZ^OJZSTJj3wqEoNzTB>{M;EW7XED(D)qZ1dea4Olq>GbTg4 zji`-fOEH!p=q~QIZL8%)f{WMpn8;|cib+A^rsaQ`V3mdryTV-(p)7vUuigD93JJ#is=yUJVa^)=NLbXU_H|LdF&^6m6{-#!iV?euz%Ce}_2^#Az$KhFNYv4{Vczh0HFE(!7vX9wFRn(*dW zrzW1Wwi7pX&7#Pq6bWcq16Ks=e*ge^zUMDrc>Gm~L2*xe< zsc+M`*%F|)5;7NZ`~a7OhEFn>*Ve7dH5aBAa<5~G{CTU<*Nb~yG~q!Q+72D>b?QvS zdEhAB9aXknN6}t!j+5@goR`k{XNgi=%L%N2SNN4ci_4I8gwjR4DXd(YqQwMb*6=JG zEiI2gijx3Eg(xr!Lo-_~EHvBm)_fQ%xmNcPyZlSW{N-2vjEm~L_YAjK&T&^sxLqlx z#*lD3YV4^JhqJdV(He`p(9uJoK~%1&;~h&T!oT&IGfFGE&+j(|pa1~j_qV;hGlP@0 zg|Uef!@u0;p97|U1k8UrQdg3W)c_%C$Th?pg5y=C#G0wm!hZ{tWZSxLy1zD~@Fi_%0=mFVi>zhk53elvN;5(xD4IVzw2qie z{XOnpE8Y8qian^Fmn5U8IGhhuFBn6H>xD@1T(Rue&h(h^cg&PWB5zcv4HcA@=rvd~ z(rE{joI!=!Ae_ntoZKKH@@YTh$6zJte2&nfF~BZbhUN{i zAG1W8(?lh@yDJqwOGR;;2055IoH9nVCn-WDGf1w|7bDS{vT4ja(}qm1+O5&&)xTQ$ z$O(k*;r<=oIJdU}@F4(zelh?6?SHl1|6{caO|97d7BnA*nJ>bVY#PNP@zF4j8JBp& zd0J+P?Pg|P)dC#KXftv_-J#wBfy`l21~6#S6CnlENh6qt?Y8yL=!7I=&tDhmt9|U} zEg%C&kGS|bXo|8p%rXfW&YZ6>+7&^<+w3U8XWXlb8 z){x~YCGIBh4}0_vk6t-uJQaVH;W3^R4~(U{=T-xOOlD{UJM054c*Cp8p+QS^8F^7`oLcNB!r1C~ z1c&z0K7_S5^~p;vzVP~Ef6P!!;r#soN>NNVg|MGzLqvgFndw@WvX!S6+1^{&DtPpB zXGw211C6qO+^@kqL9a)68AS)l#qaCN7baYu>*7l;+M}Weu4dtT95$JZ?AIqKG!{r0AlDQ%TS0qCB{dwHhyyYA^PA`XO~)On;}5fDvp<33gwHIK)g>Vwz!c zry$Pe?x52+2*0MfUuwJJ< zcble6O`7GDKET7Z{3K^wQ8f1?qK^JypQpM(gpQ@C3Z*+2)#g_Lq)Xu}e*!U`Y-P4Y zR|c~~AYA>T8VM9C&&jcY1)Pq+@3PHqhS zJL1Qe*)0DNjN}UWG`QMS<8pkjH(C7qzQ^O_>sjcdWpmH#`0JS+IJcsF7)Ze!2m#@V z0OHSdrPqmP|NRhGzcex2fNMzTKt;vk5-kIR!ag1C6=%NGSuXJOZ)^0AA8SL?@})fj zHOVF8N59VyLDu=b`0YS~aOmUr(iUJs(C0(VRamE%qP)PNo(sk|ybNU&EsfUZz^-j1 zBN8a8|Hi>1%ihsxSrN5o7nn2cT~iq_sCsa&sH#UMaptWSwa1_d9g0?`DxwYbcMP;$ zBFDFdh5@yUfw>mn5BQr#rke%`!=>H_MiJ0~1(LExUV}=Q0*$4mhtHLbA*0XArKh)E z=DcdF`(ClYKNict_%pRF>4!R3ECB#p5=lKtzn5p-AJYRi;ba83kbe`;Nxv?ao)Nc9 zZtX0X zcADi~?jCDe`qIytTB^GInapPW%`f(9mTk;re|5agxxdWv=hN%=^mXgYXWUn-p7x;J3|DgN zqZXAHwSa>?h}TUqFsL$=nS_ZfP>a!)U?R_i>|Ak{HvExQG{nnrQ`FiXMJ%>!OFRs< zj$@c}9{pehxlS)KfS(?1ajBN{syj3bCC@VN%NFx^VrMrBet-i9f}jbv z=#>+qJqjYMrhbQ58P(&+3>YyDbxS~mKTh06#(d*7#Qx@QsNb999~)uEmJ_JQ z*k3)~) zUa=j|`b2bB>bpxNPhjQ_T}y55aINH@ulB_Fh)3m|bGff5(}k)*JJA=$rppmFiBMKO z>hKRljlD(YL8b9QDK3B4;Gyw$0!tQ&0O5}sFh&mcqdq=@7WTTJ!`Sj2dmBzApD$Cw zYg*jgc;p_FV|3V=-ig`CKRQMg8)app$dTClsdPA31C+^jKX5}*nWr_!*qu+&y<{eu z3S(GflI|?VkS-jRHhCv1`kR?sh9ej+T~A0>(cu;9PfvB|C)uSN?Myeb2qC59WH%a{^ney0!wmc~Rva8u>jva1T2(Tm zr$K9&*FjP)WOdD4`sqFL@fg2{cJN4CKHzO?KEpNR1WqyzAW3?E?M+a}#kDR>oNJI_ zPOxgFAJLP&AgTo8kepQ#kuGRhbsp-XBL=~ADg5Cd<})4h%)+u(YqOF8gR=bQk4>=g z^AVcWgyO_+Y|Q^-$gSz{155YrSu7!GiL(=_q{(pwHtk93f&Y9F}gxDRS8Ej^|P

qQS72F*#t}zTB{1fK+LOxF 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 literal 0 HcmV?d00001 diff --git a/docs/static/images/logo.svg b/docs/static/images/logo.svg new file mode 100644 index 0000000..d92415c --- /dev/null +++ b/docs/static/images/logo.svg @@ -0,0 +1 @@ +sibling \ No newline at end of file From 772aebbd57220a2e2549dfad2f7076e5d23627ae Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Sun, 8 Feb 2026 18:43:41 +0900 Subject: [PATCH 05/19] publish www page in the new template --- .bin/update_version.sh | 10 +++-- .gitmodules | 3 -- docs/config.toml | 61 ++++++++++++++------------- docs/content/_index.md | 10 +++-- docs/content/about.md | 9 ++-- docs/content/install.md | 9 ++-- docs/content/usage.md | 1 + docs/go.mod | 8 ++++ docs/go.sum | 12 ++++++ docs/layouts/_default/single.html | 4 -- docs/layouts/index.html | 18 -------- docs/layouts/partials/head.html | 20 --------- docs/layouts/partials/header-nav.html | 21 --------- docs/layouts/shortcodes/logo.html | 9 ---- docs/layouts/shortcodes/version.html | 5 --- docs/static/images/logo.svg | 1 - docs/themes/cayman-hugo-theme | 1 - 17 files changed, 76 insertions(+), 126 deletions(-) create mode 100644 docs/go.mod create mode 100644 docs/go.sum delete mode 100644 docs/layouts/_default/single.html delete mode 100644 docs/layouts/index.html delete mode 100644 docs/layouts/partials/head.html delete mode 100644 docs/layouts/partials/header-nav.html delete mode 100644 docs/layouts/shortcodes/logo.html delete mode 100644 docs/layouts/shortcodes/version.html delete mode 100644 docs/static/images/logo.svg delete mode 160000 docs/themes/cayman-hugo-theme diff --git a/.bin/update_version.sh b/.bin/update_version.sh index d6e9294..a4ba19f 100755 --- a/.bin/update_version.sh +++ b/.bin/update_version.sh @@ -17,10 +17,12 @@ fi echo "$PREV_VERSION -> $VERSION (${PREV_VERSION//-/--} -> ${VERSION//-/--})" -sed -e "s#Version-v${PREV_VERSION//-/--}-information#Version-v${VERSION//-/--}-information#g" \ - -e "s#tag/v${PREV_VERSION}#tag/v${VERSION}#g" \ - -e "s#crates.io-v${PREV_VERSION}#crates.io-v${VERSION}#g" \ - README.md > a; mv a README.md +for i in README.md docs/content/_index.md ; do + sed -e "s#Version-v${PREV_VERSION//-/--}-information#Version-v${VERSION//-/--}-information#g" \ + -e "s#tag/v${PREV_VERSION}#tag/v${VERSION}#g" \ + -e "s#crates.io-v${PREV_VERSION}#crates.io-v${VERSION}#g" \ + $i > a; mv a $i +done sed -e "s#version = \".*\"#version = \"${VERSION}\"#g" docs/config.toml > a ; mv a docs/config.toml sed "s/^version = \".*\"/version = \"${VERSION}\"/g" Cargo.toml > a && mv a Cargo.toml diff --git a/.gitmodules b/.gitmodules index a79acc7..a92e211 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "themes/cayman-hugo-theme"] path = themes/cayman-hugo-theme url = https://github.com/zwbetz-gh/cayman-hugo-theme.git -[submodule "docs/themes/cayman-hugo-theme"] - path = docs/themes/cayman-hugo-theme - url = https://github.com/zwbetz-gh/cayman-hugo-theme.git diff --git a/docs/config.toml b/docs/config.toml index 0601028..ee78d6a 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -2,36 +2,39 @@ baseURL = "https://tamada.github.io/sibling" languageCode = "en-us" defaultContentLanguage = "en" title = "Sibling" -enableEmoji = true -theme = "cayman-hugo-theme" googleAnalytics = "" - -pygmentsCodefences = true -pygmentsStyle = "pygments" +theme = ["github.com/tamada/hugo_theme_mod", "github.com/nunocoracao/blowfish/v2"] [params] - project_name = "sibling" - project_logo = "images/folders.svg" - project_tagline = "get next/previous sibling directory name" - dateFormat = "2006-01-02" - katex = true - version = "2.0.5" - footer = "[![GitHub](https://img.shields.io/badge/GitHub-tamada/sibling-blueviolet.svg?logo=github)](https://github.com/tamada/sibling) Made with [Hugo](https://gohugo.io/). Theme by [Cayman](https://github.com/zwbetz-gh/cayman-hugo-theme). Deployed to [GitHub Pages](https://pages.github.com/)." +logo = "images/logo.svg" +description = "get next/previous sibling directory name." +dateFormat = "2006-01-02" +copyright = "Copyright © 2024 by Haruaki Tamada. Made with [Hugo](https://gohugo.io/). Deployed to [GitHub Pages](https://pages.github.com/)." + +[menus] +[[menus.main]] + name = "🏃 Usage" + pageRef = "/usage" + weight = 30 +[[menus.main]] + name = "⚓️ Install" + pageRef = "/install" + weight = 50 +[[menus.main]] + identifier = "haruaki-tamada" + pre = "haruaki-tamada" + url = "https://github.com/tamada/" + weight = 100 +[[menus.main]] + identifier = "github" + pre = "github" + url = "https://github.com/tamada/sibling" + weight = 200 +[[menus.footer]] + name = ":smile: About" + pageRef = "/about" + weight = 50 -[menu] - [[menu.nav]] - name = ":house: Home" - url = "/" - weight = 10 - [[menu.nav]] - name = ":anchor: Install" - url = "/install" - weight = 20 - [[menu.nav]] - name = ":runner: Usage" - url = "/usage" - weight = 30 - [[menu.nav]] - name = ":smile: About" - url = "/about" - weight = 50 \ No newline at end of file +[module] +[[module.imports]] +path = "github.com/tamada/hugo_theme_mod" \ No newline at end of file diff --git a/docs/content/_index.md b/docs/content/_index.md index 924fad0..e3d6914 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -3,13 +3,15 @@ date: 2024-07-09 title: ":house: Home" --- +{{< keywordList markdownify="true" >}} +[![crates.io](https://img.shields.io/badge/crates.io-v2.0.5-orange.svg?logo=rust)](https://crates.io/crates/sibling) +[![License](https://img.shields.io/badge/License-WTFPL-information.svg)](https://github.com/tamada/sibling/blob/master/LICENSE) +[![Version](https://img.shields.io/badge/Version-v2.0.5-information.svg)](https://github.com/tamada/sibling/releases/tag/v2.0.5) + [![build](https://github.com/tamada/sibling/actions/workflows/build.yaml/badge.svg)](https://github.com/tamada/sibling/actions/workflows/build.yaml) [![Coverage Status](https://coveralls.io/repos/github/tamada/sibling/badge.svg?branch=main)](https://coveralls.io/github/tamada/sibling?branch=main) - [![Rust Report Card](https://rust-reportcard.xuri.me/badge/github.com/tamada/sibling)](https://rust-reportcard.xuri.me/report/github.com/tamada/sibling) - -[![License](https://img.shields.io/badge/License-WTFPL-information.svg)](https://github.com/tamada/sibling/blob/master/LICENSE) -[![Version](https://img.shields.io/badge/Version-{{< version double_dash="true" >}}-information.svg)](https://github.com/tamada/sibling/releases/tag/v{{< version >}}) +{{< /keywordList >}} ## :speaking_head: Description diff --git a/docs/content/about.md b/docs/content/about.md index ff14b8b..8303fbc 100644 --- a/docs/content/about.md +++ b/docs/content/about.md @@ -1,8 +1,9 @@ --- title: ":smile: About" +date: 2024-07-09 --- -### :scroll: License +## :scroll: License * [WTFPL](https://github.com/tamada/sibling/blob/master/LICNESE) * :+1: Commercial use @@ -10,12 +11,14 @@ title: ":smile: About" * :+1: Distribution * :+1: Private use -### :man_office_worker: Developers :woman_office_worker: +## :man_office_worker: Developers :woman_office_worker: * [Haruaki Tamada](https://tamada.github.io) -### :link: Useful links for directory traversing +## :link: Useful links for directory traversing +- [zoxide](https://github.com/ajeetdsouza/zoxide) + - A smarter cd command. Supports all major shells. - [cdargs](https://github.com/cbxbiker61/cdargs) - Directory bookmarking system - Enhanced cd utilities - [Is there a directory history for bash?](https://superuser.com/questions/299694/is-there-a-directory-history-for-bash) diff --git a/docs/content/install.md b/docs/content/install.md index 6441f4b..8c2cee0 100644 --- a/docs/content/install.md +++ b/docs/content/install.md @@ -1,8 +1,9 @@ --- title: ":anchor: Installation" +date: 2024-07-09 --- -### :beer: Homebrew +## :beer: Homebrew Install `sibling` via [Homebrew](https://brew.sh), simply run: @@ -18,7 +19,7 @@ Other shell scripts are not supported, yet. eval "$(sibling --init bash)" ``` -### :muscle: Compiling yourself +## :muscle: Compiling yourself Get source codes by `git clone` or download from [GitHub](https://github.com/tamada/sibling), then run `cargo` to build `sibling` @@ -29,9 +30,9 @@ $ cd sibling $ cargo build ``` -### :briefcase: Requirements +## :briefcase: Requirements -#### Development +### Development - Rust 1.78 or later - Dependencies (See `Cargo.toml`) diff --git a/docs/content/usage.md b/docs/content/usage.md index ca20dac..0c623cd 100644 --- a/docs/content/usage.md +++ b/docs/content/usage.md @@ -1,5 +1,6 @@ --- title: ":runner: Usage" +date: 2025-12-03 --- ## :cool: Utility commands diff --git a/docs/go.mod b/docs/go.mod new file mode 100644 index 0000000..bb6307f --- /dev/null +++ b/docs/go.mod @@ -0,0 +1,8 @@ +module github.com/tamada/sibling + +go 1.25.6 + +require ( + github.com/nunocoracao/blowfish/v2 v2.97.0 // indirect + github.com/tamada/hugo_theme_mod v0.0.0-20260208080146-1497c09c3593 // indirect +) diff --git a/docs/go.sum b/docs/go.sum new file mode 100644 index 0000000..ff07e7d --- /dev/null +++ b/docs/go.sum @@ -0,0 +1,12 @@ +github.com/nunocoracao/blowfish/v2 v2.97.0 h1:ofFBkTQKFDnM5/XVLReuJ6KPzLQFwscEg0dYEebWXvw= +github.com/nunocoracao/blowfish/v2 v2.97.0/go.mod h1:4SkMc+Ht8gpQCwArqiHMBDP3soxi2OWuAhVney+cuyk= +github.com/tamada/hugo_theme_mod v0.0.0-20260208061658-30eb80995452 h1:/7cT/INXFtsoRrN75SJhc9n6W6rEudQ+La1qGPgrqys= +github.com/tamada/hugo_theme_mod v0.0.0-20260208061658-30eb80995452/go.mod h1:9X6na3BQHPMbdY85ViJZuM6OxnPEjQ/073zT26mDZKc= +github.com/tamada/hugo_theme_mod v0.0.0-20260208070750-39a85c2d7cc1 h1:LT6HAq8FLDw8iGW1yFVpNEMW2gFGauDLWWgWcwv/xx0= +github.com/tamada/hugo_theme_mod v0.0.0-20260208070750-39a85c2d7cc1/go.mod h1:9X6na3BQHPMbdY85ViJZuM6OxnPEjQ/073zT26mDZKc= +github.com/tamada/hugo_theme_mod v0.0.0-20260208075559-9ad21fe5c7a6 h1:jxMxw5sgZscY+fui/MeTGLQJZ4zzxMOFmtfch8rR84g= +github.com/tamada/hugo_theme_mod v0.0.0-20260208075559-9ad21fe5c7a6/go.mod h1:9X6na3BQHPMbdY85ViJZuM6OxnPEjQ/073zT26mDZKc= +github.com/tamada/hugo_theme_mod v0.0.0-20260208075822-8b99fee04b44 h1:hZyXSMFkfdXkeVXxoJhAjZQHll7t0BoIZcMih9yhTgw= +github.com/tamada/hugo_theme_mod v0.0.0-20260208075822-8b99fee04b44/go.mod h1:9X6na3BQHPMbdY85ViJZuM6OxnPEjQ/073zT26mDZKc= +github.com/tamada/hugo_theme_mod v0.0.0-20260208080146-1497c09c3593 h1:PY+EasAh+2UdYzMs/uq49J+wIRDIsqDUvdCa2r+gVn8= +github.com/tamada/hugo_theme_mod v0.0.0-20260208080146-1497c09c3593/go.mod h1:9X6na3BQHPMbdY85ViJZuM6OxnPEjQ/073zT26mDZKc= diff --git a/docs/layouts/_default/single.html b/docs/layouts/_default/single.html deleted file mode 100644 index ace1256..0000000 --- a/docs/layouts/_default/single.html +++ /dev/null @@ -1,4 +0,0 @@ -{{ define "main" }} -

{{ .Title | emojify }}

- {{ .Content }} -{{ end }} diff --git a/docs/layouts/index.html b/docs/layouts/index.html deleted file mode 100644 index d7d8b4e..0000000 --- a/docs/layouts/index.html +++ /dev/null @@ -1,18 +0,0 @@ -{{ define "main" }} -

{{ .Title | emojify }}

- -{{ .Content }} - -
    -{{ $pages := where site.RegularPages "Type" "in" site.Params.mainSections }} -{{ range $pages.ByPublishDate.Reverse }} -
  • - {{ $dateFormat := $.Site.Params.dateFormat | default "Jan 2, 2006" }} - {{ .PublishDate.Format $dateFormat }} - - {{ .Title }} - -
  • -{{ end }} -
-{{ end }} diff --git a/docs/layouts/partials/head.html b/docs/layouts/partials/head.html deleted file mode 100644 index b1db87d..0000000 --- a/docs/layouts/partials/head.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - {{ hugo.Generator }} - - - {{ $caymanScss := "scss/cayman.scss" }} - {{ $options := (dict "targetPath" "css/cayman.css") }} - {{ $caymanCss := resources.Get $caymanScss | resources.ExecuteAsTemplate $caymanScss . | toCSS $options | fingerprint }} - - - - - - - - {{ partial "katex.html" . }} - {{ .Title | emojify }} | {{ $.Site.Title }} - diff --git a/docs/layouts/partials/header-nav.html b/docs/layouts/partials/header-nav.html deleted file mode 100644 index 5474ebc..0000000 --- a/docs/layouts/partials/header-nav.html +++ /dev/null @@ -1,21 +0,0 @@ - diff --git a/docs/layouts/shortcodes/logo.html b/docs/layouts/shortcodes/logo.html deleted file mode 100644 index 2373690..0000000 --- a/docs/layouts/shortcodes/logo.html +++ /dev/null @@ -1,9 +0,0 @@ -{{- $img := .Get "src" }} -{{- $width := .Get "width" }} -{{- $caption := .Get "caption" }} -
- -
- {{ $caption | markdownify }} -
-
diff --git a/docs/layouts/shortcodes/version.html b/docs/layouts/shortcodes/version.html deleted file mode 100644 index 9eb493b..0000000 --- a/docs/layouts/shortcodes/version.html +++ /dev/null @@ -1,5 +0,0 @@ -{{- $version := $.Site.Params.version }} -{{- if eq (.Get "double_dash") "true" -}} -{{- $version = strings.Replace $version "-" "--" -}} -{{- end -}} -{{ $version }} \ No newline at end of file diff --git a/docs/static/images/logo.svg b/docs/static/images/logo.svg deleted file mode 100644 index d92415c..0000000 --- a/docs/static/images/logo.svg +++ /dev/null @@ -1 +0,0 @@ -sibling \ No newline at end of file diff --git a/docs/themes/cayman-hugo-theme b/docs/themes/cayman-hugo-theme deleted file mode 160000 index 73140a8..0000000 --- a/docs/themes/cayman-hugo-theme +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 73140a8ea8beecefbbca393f0a34cf4bb1b5387c From 3b1bdb0a69ecb6157252608b96d41d27ca97d0a1 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Mon, 9 Feb 2026 20:22:40 +0900 Subject: [PATCH 06/19] add icon materials and hugo config --- docs/config.toml | 4 +- .../rodentia-icons_folder-drag-accept.svg | 58 +++++++++++++++++++ .../rodentia-icons_folder-visiting-green.svg | 55 ++++++++++++++++++ .../rodentia-icons_folder-visiting-red.svg | 55 ++++++++++++++++++ 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 docs/resources/rodentia-icons_folder-drag-accept.svg create mode 100644 docs/resources/rodentia-icons_folder-visiting-green.svg create mode 100644 docs/resources/rodentia-icons_folder-visiting-red.svg diff --git a/docs/config.toml b/docs/config.toml index ee78d6a..eb0abd1 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -23,7 +23,7 @@ copyright = "Copyright © 2024 by Haruaki Tamada. Made with [Hugo](https://g [[menus.main]] identifier = "haruaki-tamada" pre = "haruaki-tamada" - url = "https://github.com/tamada/" + url = "https://tamada.github.io" weight = 100 [[menus.main]] identifier = "github" @@ -37,4 +37,4 @@ copyright = "Copyright © 2024 by Haruaki Tamada. Made with [Hugo](https://g [module] [[module.imports]] -path = "github.com/tamada/hugo_theme_mod" \ No newline at end of file +path = "github.com/tamada/hugo_theme_mod" diff --git a/docs/resources/rodentia-icons_folder-drag-accept.svg b/docs/resources/rodentia-icons_folder-drag-accept.svg new file mode 100644 index 0000000..5a2013e --- /dev/null +++ b/docs/resources/rodentia-icons_folder-drag-accept.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/resources/rodentia-icons_folder-visiting-green.svg b/docs/resources/rodentia-icons_folder-visiting-green.svg new file mode 100644 index 0000000..7855ca5 --- /dev/null +++ b/docs/resources/rodentia-icons_folder-visiting-green.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/resources/rodentia-icons_folder-visiting-red.svg b/docs/resources/rodentia-icons_folder-visiting-red.svg new file mode 100644 index 0000000..c46688d --- /dev/null +++ b/docs/resources/rodentia-icons_folder-visiting-red.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 81348c2dad5f03553ad448fdaae45cf72490680b Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Wed, 11 Feb 2026 15:32:02 +0900 Subject: [PATCH 07/19] refactor: reorganize code structure and update dependencies for improved functionality --- Cargo.toml | 29 +------- assets/init/init.bash | 47 ++++++------- cli/src/cli.rs | 42 +++++++---- cli/src/main.rs | 33 +++++---- cli/src/minisib.rs | 159 ++++++++++++++++++++++++++++++++++++++++++ cli/src/printer.rs | 5 +- lib/Cargo.toml | 2 +- lib/src/lib.rs | 33 ++++++--- 8 files changed, 255 insertions(+), 95 deletions(-) create mode 100644 cli/src/minisib.rs diff --git a/Cargo.toml b/Cargo.toml index bbe319e..db57b60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,31 +13,4 @@ authors = [ ] license-file = "LICENSE" categories = ["command-line-utilities"] -edition = "2021" - -# Config for 'cargo dist' -[workspace.metadata.dist] -# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.19.1" -# CI backends to support -ci = "github" -# The installers to generate for each app -installers = ["shell", "powershell", "npm", "homebrew", "msi"] -# A GitHub repo to push Homebrew formulas to -tap = "tamada/homebrew-tap" -# Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] -# The archive format to use for windows builds (defaults .zip) -windows-archive = ".tar.gz" -# The archive format to use for non-windows builds (defaults .tar.xz) -unix-archive = ".tar.gz" -# A namespace to use when publishing this package to the npm registry -npm-scope = "@htamada" -# Path that installers should place binaries in -install-path = "CARGO_HOME" -# Publish jobs to run in CI -publish-jobs = ["homebrew"] -# Publish jobs to run in CI -pr-run-mode = "plan" -# Whether to install an updater program -install-updater = false +edition = "2024" diff --git a/assets/init/init.bash b/assets/init/init.bash index c345903..f365d67 100644 --- a/assets/init/init.bash +++ b/assets/init/init.bash @@ -1,26 +1,26 @@ -__change_directory_to_sibling() { +__change_directory_with_sibling() { traversing_type="$1" if [[ "$traversing_type" == "" ]]; then traversing_type="next" + else + shift fi - step=1 - if [[ $# -eq 2 ]]; then - step=$2 - fi - result=$(sibling --type $traversing_type --csv --step $step) + eval "$(sibling minisib $traversing_type $@ | { + read -r next; echo "next='$next';" + read -r length; echo "length='$length';" + read -r current; echo "current='$current';" + read -r last_flag; echo "last_flag='$last_flag';" + })" sibling_status=$? - echo $result | while IFS=, read current next ci ni total - do - # echo "Current: $current, Next: $next, ci: $ci, ni: $ni, total: $total" - if [[ $sibling_status -eq 0 ]] ; then - # strip the first and last double quotes - cd "$(echo $next | sed -e 's/^"//' -e 's/"$//')" - echo "$PWD ($ci/$total)" - else - echo "Done ($ci/$total)" - cd .. - fi - done + # echo "Current: $current, Next: $next, ci: $ci, ni: $ni, total: $total" + if [[ $sibling_status -eq 0 ]] ; then + # strip the first and last double quotes + cd $next + echo "$PWD (${current}/${length})" + else + echo "Done (${current}/${length})" + cd .. + fi return $sibling_status } @@ -84,22 +84,21 @@ sibling_fzf() { } cdfirst() { - __change_directory_to_sibling first + __change_directory_with_sibling first $@ } cdlast() { - __change_directory_to_sibling last + __change_directory_with_sibling last $@ } cdnext() { - __change_directory_to_sibling next $@ + __change_directory_with_sibling next $@ } cdprev() { - __change_directory_to_sibling previous $@ + __change_directory_with_sibling previous $@ } cdrand() { - __change_directory_to_sibling random + __change_directory_with_sibling random $@ } - diff --git a/cli/src/cli.rs b/cli/src/cli.rs index bd2af7b..88f3ecd 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use clap::{Parser, ValueEnum}; -use crate::LogLevel; +use crate::{LogLevel, minisib}; #[derive(Debug, Parser)] #[clap(version, author, about, arg_required_else_help = true)] @@ -10,14 +10,8 @@ pub struct CliOpts { #[clap(flatten)] pub(crate) p_opts: PrintingOpts, - #[arg( - short, - long, - help = "specify the number of times to execute sibling", - value_name = "COUNT", - default_value_t = 1 - )] - pub step: usize, + #[clap(flatten)] + pub(crate) nexter_opts: NexterOpts, #[arg( long, @@ -38,6 +32,28 @@ pub struct CliOpts { )] pub init: Option, + #[arg(index = 1, help = "the target directory", value_name = "DIR")] + pub dirs: Vec, + + #[cfg(debug_assertions)] + #[clap(flatten)] + pub(crate) compopts: CompletionOpts, + + #[clap(subcommand)] + pub(crate) minisib: Option, +} + +#[derive(Parser, Debug)] +pub(crate) struct NexterOpts { + #[arg( + short, + long, + help = "specify the number of times to execute sibling", + value_name = "COUNT", + default_value_t = 1 + )] + pub step: i32, + #[arg(short = 't', long = "type", help = "specify the nexter type", value_enum, default_value_t = sibling::NexterType::Next, value_name = "TYPE", ignore_case = true)] pub nexter: sibling::NexterType, @@ -49,12 +65,8 @@ pub struct CliOpts { )] pub input: Option, - #[arg(index = 1, help = "the target directory", value_name = "DIR")] - pub dirs: Vec, - - #[cfg(debug_assertions)] - #[clap(flatten)] - pub(crate) compopts: CompletionOpts, + #[arg(short = 'S', long, help = "strictly check the directory traversing", default_value_t = false)] + pub strict: bool, } #[cfg(debug_assertions)] diff --git a/cli/src/main.rs b/cli/src/main.rs index fb791c7..30234c0 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -8,6 +8,7 @@ mod cli; mod gencomp; mod init; mod printer; +pub(crate) mod minisib; #[derive(clap::ValueEnum, Clone, Debug)] pub enum LogLevel { @@ -21,12 +22,14 @@ pub enum LogLevel { fn init_log(level: &LogLevel) { use LogLevel::{Error, Warn, Info, Debug, Trace}; if std::env::var_os("RUST_LOG").is_none() { - match level { - Error => std::env::set_var("RUST_LOG", "error"), - Warn => std::env::set_var("RUST_LOG", "warn"), - Info => std::env::set_var("RUST_LOG", "info"), - Debug => std::env::set_var("RUST_LOG", "debug"), - Trace => std::env::set_var("RUST_LOG", "trace"), + unsafe { + match level { + Error => std::env::set_var("RUST_LOG", "error"), + Warn => std::env::set_var("RUST_LOG", "warn"), + Info => std::env::set_var("RUST_LOG", "info"), + Debug => std::env::set_var("RUST_LOG", "debug"), + Trace => std::env::set_var("RUST_LOG", "trace"), + }; } } env_logger::init(); @@ -36,7 +39,7 @@ fn init_log(level: &LogLevel) { fn perform_impl( dirs: &Dirs, nexter: &dyn Nexter, - step: usize, + step: i32, opts: &PrintingOpts, ) -> String { let next = dirs.next_with(nexter, step); @@ -44,12 +47,12 @@ fn perform_impl( } fn perform_from_file(opts: CliOpts) -> Vec> { - let nexter = sibling::NexterFactory::create(opts.nexter); - let r = match opts.input { + let nexter = sibling::NexterFactory::create(opts.nexter_opts.nexter); + let r = match opts.nexter_opts.input { None => Err(Error::Fatal("input is not specified".into())), Some(file) => match Dirs::new_from_file(file) { Err(e) => Err(e), - Ok(dirs) => Ok(perform_impl(&dirs, nexter.as_ref(), opts.step, &opts.p_opts)), + Ok(dirs) => Ok(perform_impl(&dirs, nexter.as_ref(), opts.nexter_opts.step, &opts.p_opts)), }, }; vec![r] @@ -58,7 +61,7 @@ fn perform_from_file(opts: CliOpts) -> Vec> { fn perform_each( dir: std::path::PathBuf, nexter: &dyn Nexter, - step: usize, + step: i32, opts: &PrintingOpts, ) -> Result { match Dirs::new(dir) { @@ -68,7 +71,7 @@ fn perform_each( } fn perform_sibling(opts: CliOpts) -> Vec> { - let nexter = sibling::NexterFactory::create(opts.nexter); + let nexter = sibling::NexterFactory::create(opts.nexter_opts.nexter); let target_dirs = if opts.dirs.is_empty() { vec![std::env::current_dir().unwrap()] } else { @@ -81,7 +84,7 @@ fn perform_sibling(opts: CliOpts) -> Vec> { } else { dir }; - let r = perform_each(dir, nexter.as_ref(), opts.step, &opts.p_opts); + let r = perform_each(dir, nexter.as_ref(), opts.nexter_opts.step, &opts.p_opts); result.push(r); } result @@ -90,8 +93,10 @@ fn perform_sibling(opts: CliOpts) -> Vec> { fn perform(opts: CliOpts) -> Vec> { if let Some(shell) = opts.init { vec![init::generate_init_script(&shell)] - } else if opts.input.is_some() { + } else if opts.nexter_opts.input.is_some() { perform_from_file(opts) + } else if let Some(minisib) = opts.minisib { + vec![minisib.perform()] } else { perform_sibling(opts) } diff --git a/cli/src/minisib.rs b/cli/src/minisib.rs new file mode 100644 index 0000000..75ed1d4 --- /dev/null +++ b/cli/src/minisib.rs @@ -0,0 +1,159 @@ +//! minisib: A minimal sibling command implementation +//! +//! This program is used for directory traversing in the shell scripts. +//! +//! Usage: +//! minisib [OPTIONS] [INPUT_FILE] +//! OPTIONS: +//! : specify the number of times to execute sibling (default: 1) +//! -1 means minus one step, -2 means minus two steps, and +2 means plus two steps. +//! NEXTER_TYPE: next, previous, first, last, random, keep +//! INPUT_FILE: file containing the list of directories (if not provided, uses current directory +use sibling::{Dirs, Error, NexterType, Result}; +use clap::{Subcommand, Parser}; + +#[derive(Subcommand, Debug)] +pub(crate) enum MiniSibCommand { + #[command(name = "minisib", about = "Great Finding! A minimal sibling command implementation. This command is a helper utility for the shell. It assumes the user will not use it.", hide = true)] + MiniSib(RawOpts), +} + +impl MiniSibCommand { + pub(crate) fn perform(&self) -> Result { + match self { + MiniSibCommand::MiniSib(opts) => { + opts.parse().and_then(|item| item.perform()) + } + } + } +} + +#[derive(Parser, Debug)] +pub(crate) struct RawOpts { + #[arg(allow_hyphen_values = true)] + args: Vec, +} + +impl RawOpts { + pub(crate) fn parse(&self) -> Result { + if self.args.is_empty() { + Err(Error::Fatal("NEXTER_TYPE is required".into())) + } else if self.args.len() == 1 { + let nexter = NexterType::from(self.args[0].as_str()); + Ok(MiniSibOpts::new(nexter, 1, None)) + } else if self.args.len() == 2 { + let nexter = NexterType::from(self.args.first().unwrap().as_str()); + if let Ok(n) = self.args[1].parse::() { + let step = n; + Ok(MiniSibOpts::new(nexter, step, None)) + } else { + let file = self.args[1].as_str().to_string(); + Ok(MiniSibOpts::new(nexter, 1, Some(file))) + } + } else { + let nexter = NexterType::from(self.args.first().unwrap().as_str()); + if let Ok(n) = self.args[1].parse::() { + let step = n; + let file = Some(self.args[2].as_str().to_string()); + Ok(MiniSibOpts::new(nexter, step, file)) + } else if let Ok(n) = self.args[2].parse::() { + let step = n; + let file = Some(self.args[1].as_str().to_string()); + Ok(MiniSibOpts::new(nexter, step, file)) + } else { + Err(Error::Fatal("Invalid step argument".into())) + } + } + } +} + +pub(crate) struct MiniSibOpts { + pub(crate) nexter_type: NexterType, + pub(crate) step: i32, + pub(crate) file: Option, +} + +impl MiniSibOpts { + pub(crate) fn new(nexter_type: NexterType, step: i32, file: Option) -> Self { + MiniSibOpts { + nexter_type, + step, + file, + } + } + + pub(crate) fn perform(&self) -> Result { + let nexter = sibling::NexterFactory::create(self.nexter_type.clone()); + let target_dirs = if let Some(file) = &self.file { + Dirs::new_from_file(file)? + } else { + Dirs::new(std::env::current_dir().unwrap())? + }; + if let Some(dir) = target_dirs.next_with(nexter.as_ref(), self.step) { + Ok(format!("{}\n{}\n{}\n{}", + dir.path().to_string_lossy(), + target_dirs.len(), + dir.index(), + dir.is_last_item())) + } else { + Err(Error::Fatal("No sibling directory found".into())) + } + + } +} + +#[cfg(test)] +mod tests { + use clap::Parser; + + use crate::minisib::MiniSibCommand; + + #[test] + fn test_args_1() { + let args = vec!["sibling", "minisib", "previous", "-2", "dirs.txt"]; + let minisib = crate::cli::CliOpts::parse_from(&args) + .minisib; + assert!(minisib.is_some()); + let opts = if let Some(MiniSibCommand::MiniSib(opts)) = minisib { + opts.parse().unwrap() + } else { + panic!("Expected MiniSibCommand::MiniSib"); + }; + + assert_eq!(opts.step, -2); + assert_eq!(opts.nexter_type, super::NexterType::Previous); + assert_eq!(opts.file, Some("dirs.txt".into())); + } + + #[test] + fn test_args_2() { + let args = vec!["sibling", "minisib", "previous", "dirs.txt"]; + let minisib = crate::cli::CliOpts::parse_from(&args) + .minisib; + assert!(minisib.is_some()); + let opts = if let Some(MiniSibCommand::MiniSib(opts)) = minisib { + opts.parse().unwrap() + } else { + panic!("Expected MiniSibCommand::MiniSib"); + }; + assert_eq!(opts.step, 1); + assert_eq!(opts.nexter_type, super::NexterType::Previous); + assert_eq!(opts.file, Some("dirs.txt".into())); + } + + #[test] + fn test_args_3() { + let args = vec!["sibling", "minisib", "keep"]; + let minisib = crate::cli::CliOpts::parse_from(&args) + .minisib; + assert!(minisib.is_some()); + let opts = if let Some(MiniSibCommand::MiniSib(opts)) = minisib { + opts.parse().unwrap() + } else { + panic!("Expected MiniSibCommand::MiniSib"); + }; + assert_eq!(opts.step, 1); + assert_eq!(opts.nexter_type, super::NexterType::Keep); + assert!(opts.file.is_none()); + } +} \ No newline at end of file diff --git a/cli/src/printer.rs b/cli/src/printer.rs index 4203350..e6cb83b 100644 --- a/cli/src/printer.rs +++ b/cli/src/printer.rs @@ -78,7 +78,7 @@ fn list_string(dirs: &Dirs, next: Option<&Dir<'_>>, opts: &PrintingOpts) -> Stri } fn result_string_impl(dirs: &Dirs, next: Option>, opts: &PrintingOpts) -> String { - let r = if opts.progress { + if opts.progress { format!( "{} ({}/{})", pathbuf_to_string(next.as_ref().map(Dir::path), opts.absolute), @@ -87,8 +87,7 @@ fn result_string_impl(dirs: &Dirs, next: Option>, opts: &PrintingOpts) - ) } else { pathbuf_to_string(next.as_ref().map(Dir::path), opts.absolute).to_string() - }; - r + } } fn pathbuf_to_string(path: Option<&Path>, absolute: bool) -> String { diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 165a4da..5059dd4 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -12,7 +12,7 @@ homepage.workspace = true license-file.workspace = true [dependencies] -rand = "0.9.0" +rand = "0.10.0" rust-embed = "8.5.0" clap = { version = "4.5.23", features = ["derive"] } log = "0.4.29" diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 3f4ea4b..fb60c38 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -39,6 +39,26 @@ pub enum NexterType { Keep, } +impl From<&str> for NexterType { + fn from(s: &str) -> Self { + match s.to_lowercase().as_str() { + "first" => NexterType::First, + "last" => NexterType::Last, + "previous" => NexterType::Previous, + "next" => NexterType::Next, + "random" => NexterType::Random, + "keep" => NexterType::Keep, + _ => NexterType::Next, + } + } +} + +impl From for NexterType { + fn from(s: String) -> Self { + NexterType::from(s.as_str()) + } +} + /// The error type for sibling. #[derive(Debug)] pub enum Error { @@ -241,13 +261,8 @@ impl Dirs { } /// Get the next directory using the given [`Nexter`] and step. - /// - /// ## Panics - /// - /// This function will panic if the step exceeds `i32::MAX`. - #[must_use] - pub fn next_with(&self, nexter: &dyn Nexter, step: usize) -> Option> { - nexter.next_with(self, i32::try_from(step).unwrap()) + pub fn next_with(&self, nexter: &dyn Nexter, step: i32) -> Option> { + nexter.next_with(self, step) } /// Get an iterator over the directories. @@ -450,9 +465,7 @@ impl Nexter for Next { impl Nexter for Random { fn next_with<'a>(&self, dirs: &'a Dirs, _step: i32) -> Option> { - use rand::Rng; - let mut rng = rand::rng(); - let next = rng.random_range(0..dirs.len()); + let next = rand::random_range(0..dirs.len()); log::trace!("Random::next_with -> index {next}"); Some(Dir::new(dirs, next)) } From 61c4b3afd366fac503f14988085478fb8d2f9b76 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 10:28:30 +0900 Subject: [PATCH 08/19] refactor: enhance directory handling and update CLI options for improved functionality --- cli/src/cli.rs | 6 +- cli/src/main.rs | 4 +- cli/src/minisib.rs | 8 +- cli/src/printer.rs | 24 +++--- lib/Cargo.toml | 3 + lib/src/lib.rs | 113 +++++++++++++++++------------ lib/tests/common/mod.rs | 8 ++ lib/tests/skip_non_existent_dir.rs | 31 ++++++++ testdata/basic/dirlist.txt | 8 +- testdata/demo/.init_demo.sh | 2 + testdata/demo/target.txt | 5 ++ testdata/skip.txt | 4 + 12 files changed, 147 insertions(+), 69 deletions(-) create mode 100644 lib/tests/common/mod.rs create mode 100644 lib/tests/skip_non_existent_dir.rs create mode 100644 testdata/demo/.init_demo.sh create mode 100644 testdata/demo/target.txt create mode 100644 testdata/skip.txt diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 88f3ecd..7d63c5b 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -65,8 +65,8 @@ pub(crate) struct NexterOpts { )] pub input: Option, - #[arg(short = 'S', long, help = "strictly check the directory traversing", default_value_t = false)] - pub strict: bool, + #[arg(short = 'a', long, help = "Set the targets to all directories from the given list. By default, the sibling skips non-existent directories.", default_value_t = false)] + pub all: bool, } #[cfg(debug_assertions)] @@ -108,7 +108,7 @@ pub(crate) struct PrintingOpts { pub format: Format, #[arg( - short, + short = 'A', long, help = "print the directory name in the absolute path", default_value_t = false diff --git a/cli/src/main.rs b/cli/src/main.rs index 30234c0..d266108 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -7,7 +7,7 @@ use sibling::{Dirs, Error, Nexter, Result}; mod cli; mod gencomp; mod init; -mod printer; +pub(crate) mod printer; pub(crate) mod minisib; #[derive(clap::ValueEnum, Clone, Debug)] @@ -50,7 +50,7 @@ fn perform_from_file(opts: CliOpts) -> Vec> { let nexter = sibling::NexterFactory::create(opts.nexter_opts.nexter); let r = match opts.nexter_opts.input { None => Err(Error::Fatal("input is not specified".into())), - Some(file) => match Dirs::new_from_file(file) { + Some(file) => match Dirs::new_from_file_with(file, opts.nexter_opts.all) { Err(e) => Err(e), Ok(dirs) => Ok(perform_impl(&dirs, nexter.as_ref(), opts.nexter_opts.step, &opts.p_opts)), }, diff --git a/cli/src/minisib.rs b/cli/src/minisib.rs index 75ed1d4..3de2f93 100644 --- a/cli/src/minisib.rs +++ b/cli/src/minisib.rs @@ -91,12 +91,16 @@ impl MiniSibOpts { }; if let Some(dir) = target_dirs.next_with(nexter.as_ref(), self.step) { Ok(format!("{}\n{}\n{}\n{}", - dir.path().to_string_lossy(), + crate::printer::pathbuf_to_string(Some(dir.path()), false, target_dirs.on_dirs), target_dirs.len(), dir.index(), dir.is_last_item())) } else { - Err(Error::Fatal("No sibling directory found".into())) + Ok(format!("{}\n{}\n{}\n{}", + crate::printer::pathbuf_to_string(Some(target_dirs.parent()), false, target_dirs.on_dirs), + target_dirs.len(), + -1, + true)) } } diff --git a/cli/src/printer.rs b/cli/src/printer.rs index e6cb83b..ae7078b 100644 --- a/cli/src/printer.rs +++ b/cli/src/printer.rs @@ -24,10 +24,10 @@ pub(crate) fn result_string( fn json_string(dirs: &Dirs, next: Option>, absolute: bool) -> String { let current = dirs.current(); - let next_path = next.as_ref().map(|n| pathbuf_to_string(Some(n.path()), absolute)); + let next_path = next.as_ref().map(|n| pathbuf_to_string(Some(n.path()), absolute, dirs.on_dirs)); format!( r#"{{"current":{{"path":"{}","index":{}}},"next":{{"path":"{}","index":{}}},"total":{}}}"#, - pathbuf_to_string(Some(dirs.current().path()), absolute), + pathbuf_to_string(Some(dirs.current().path()), absolute, dirs.on_dirs), current.index() + 1, next_path.unwrap_or_default(), next.map_or(-1, |n| i32::try_from(n.index()).unwrap() + 1), @@ -39,8 +39,8 @@ fn csv_string(dirs: &Dirs, next: Option>, absolute: bool) -> String { let current = dirs.current(); format!( r#""{}","{}",{},{},{}"#, - pathbuf_to_string(Some(dirs.current().path()), absolute), - pathbuf_to_string(next.as_ref().map(Dir::path), absolute), + pathbuf_to_string(Some(dirs.current().path()), absolute, dirs.on_dirs), + pathbuf_to_string(next.as_ref().map(Dir::path), absolute, dirs.on_dirs), current.index() + 1, next.map_or(-1, |n| i32::try_from(n.index()).unwrap() + 1), dirs.len() @@ -49,7 +49,7 @@ fn csv_string(dirs: &Dirs, next: Option>, absolute: bool) -> String { fn no_more_dir_string(dirs: &Dirs, opts: &PrintingOpts) -> String { if opts.parent { - pathbuf_to_string(Some(dirs.parent()), opts.absolute) + pathbuf_to_string(Some(dirs.parent()), opts.absolute, dirs.on_dirs) } else { String::from("no more sibling directory") } @@ -71,7 +71,7 @@ fn list_string(dirs: &Dirs, next: Option<&Dir<'_>>, opts: &PrintingOpts) -> Stri "{:>4} {}{}", i + 1, prefix, - pathbuf_to_string(Some(dir), opts.absolute) + pathbuf_to_string(Some(dir), opts.absolute, dirs.on_dirs) )); } result.join("\n") @@ -81,16 +81,16 @@ fn result_string_impl(dirs: &Dirs, next: Option>, opts: &PrintingOpts) - if opts.progress { format!( "{} ({}/{})", - pathbuf_to_string(next.as_ref().map(Dir::path), opts.absolute), + pathbuf_to_string(next.as_ref().map(Dir::path), opts.absolute, dirs.on_dirs), next.map_or(-1, |n| i32::try_from(n.index()).unwrap()) + 1, dirs.len() ) } else { - pathbuf_to_string(next.as_ref().map(Dir::path), opts.absolute).to_string() + pathbuf_to_string(next.as_ref().map(Dir::path), opts.absolute, dirs.on_dirs).to_string() } } -fn pathbuf_to_string(path: Option<&Path>, absolute: bool) -> String { +pub(crate) fn pathbuf_to_string(path: Option<&Path>, absolute: bool, on_dirs: bool) -> String { match path { Some(p) => { if absolute { @@ -99,7 +99,11 @@ fn pathbuf_to_string(path: Option<&Path>, absolute: bool) -> String { .to_string_lossy() .to_string() } else { - p.to_string_lossy().to_string() + if on_dirs && !p.is_absolute() { + format!("../{}", p.to_string_lossy()) + } else { + p.to_string_lossy().to_string() + } } } None => String::new(), diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 5059dd4..7faf013 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -16,3 +16,6 @@ rand = "0.10.0" rust-embed = "8.5.0" clap = { version = "4.5.23", features = ["derive"] } log = "0.4.29" + +[dev-dependencies] +env_logger = "0.11.8" diff --git a/lib/src/lib.rs b/lib/src/lib.rs index fb60c38..96d1e7f 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -104,6 +104,8 @@ pub struct Dirs { entries: Vec, /// The parent directory that contains all the sibling directories. parent: PathBuf, + /// Flag indicating whether the current directory is among the sibling directories. + pub on_dirs: bool, /// The index of the current directory in the `dirs` vector. current: usize, } @@ -167,7 +169,7 @@ impl Dirs { /// /// # Returns /// - /// A [`Result`]<[`Dirs`]> instance. + /// A [`Result`]<[`Dirs`], [`Error`]> instance. /// /// # Errors /// @@ -202,34 +204,41 @@ impl Dirs { /// Create a new [`Dirs`] instance from the given file. /// The file should contain a list of directories, one per line. /// The first line can optionally specify the parent directory in the format `parent:/path/to`. + /// If the current directory is not found in the list, the current index is set to 0. + /// If `all_target` parameter is false, non-existent directories are skipped and + /// the resultant list does not include them. /// /// # Returns - /// A [`Result`]<[`Dirs`]> instance. + /// A [`Result`]<[`Dirs`], [`Error`]> instance. /// /// # Errors /// /// - Returns [`Error::Io`] if an I/O error occurs. /// - Returns [`Error::NotFile`] if the given path is not a file. /// - Returns [`Error::NotFound`] if the given path does not exist. - pub fn new_from_file>(file: S) -> Result { + pub fn new_from_file_with>(file: S, all_target: bool) -> Result { log::debug!("Dirs::new_from_file(file={})", file.as_ref()); let file = file.as_ref(); if file == "-" { log::info!("Reading directories from stdin"); - return Ok(build_from_reader(Box::new(std::io::stdin().lock()))); + return Ok(build_from_reader(Box::new(std::io::stdin().lock()), all_target)); } let path = PathBuf::from(file); if !path.exists() { - log::error!("Dirs::new_from_file: Not found: {}", path.display()); + log::error!("Dirs::new_from_file: Not found: {}, pwd: {}", path.display(), std::env::current_dir().unwrap().display()); Err(Error::NotFound(path)) } else if path.is_dir() { log::error!("Dirs::new_from_file: Not a file: {}", path.display()); Err(Error::NotFile(path)) } else { - build_from_list(&path) + build_from_list(&path, all_target) } } + pub fn new_from_file>(file: S) -> Result { + Dirs::new_from_file_with(file, false) + } + /// Get the parent directory path. #[must_use] pub fn parent(&self) -> &Path { @@ -281,20 +290,12 @@ fn build_dirs(parent: Option<&Path>, current: PathBuf) -> Result { let mut errs = vec![]; let dirs = collect_dirs(parent, &mut errs); if errs.is_empty() { - let current_index = find_current(&dirs, ¤t); - let index = if current_index == -1 { - log::warn!( - "build_dirs: current directory not found in siblings: {}", - current.display() - ); - 0 - } else { - usize::try_from(current_index).unwrap() - }; + let (index, on_dirs) = find_current(&dirs, ¤t); log::info!("build_dirs: siblings={}, current_index={index}", dirs.len()); Ok(Dirs { entries: dirs, parent: parent.to_path_buf(), + on_dirs: on_dirs, current: index, }) } else { @@ -330,44 +331,60 @@ fn collect_dirs(parent: &Path, errs: &mut Vec) -> Vec { } /// Return the index of current in dirs, or 0 if not found. -fn find_current(dirs: &[PathBuf], current: &PathBuf) -> i32 { +fn find_current(dirs: &[PathBuf], current: &PathBuf) -> (usize, bool) { let idx = dirs .iter() - .position(|dir| dir == current) - .map_or(-1, |i| i32::try_from(i).unwrap()); - log::trace!("find_current: index={} for {}", idx, current.display()); - idx + .position(|dir| dir == current); + if let Some(index) = idx { + log::trace!("find_current: found current at index {index}"); + (index, true) + } else { + log::warn!("find_current: current directory not found in siblings"); + (0, false) + } } /// Parse lines from a reader; parent: sets base, remaining lines are directory entries. -fn build_from_reader(reader: Box) -> Dirs { - let lines = reader - .lines() - .filter_map(|line| line.map(|n| n.trim().to_string()).ok()) - .collect::>(); - let base = if let Some(base) = lines.iter().find(|l| l.starts_with("parent:")) { - base.chars().skip(7).collect::().trim().to_string() - } else { - ".".to_string() - }; - let dirs = lines - .iter() - .filter(|l| !l.starts_with("parent:")) - .map(PathBuf::from) - .collect::>(); - log::debug!("build_from_reader: base='{}', entries={}", base, dirs.len()); - let current = find_current_dir_index(&dirs); - if current == 0 { - log::warn!("build_from_reader: current directory not found in siblings"); +fn build_from_reader(reader: Box, all_target: bool) -> Dirs { + let mut parent = String::from("."); + let mut lines = vec![]; + for line in reader.lines() { + if let Ok(line) = line { + if line.starts_with("parent:") { + parent = line.chars().skip("parent:".len()).collect::().trim().to_string(); + } else { + let p = Path::new(line.trim()); + if !all_target && not_exists(p) { + continue; + } else { + lines.push(p.to_path_buf()); + } + } + } + } + log::debug!("build_from_reader: base='{}', entries={}", parent, lines.len()); + let (current, on_dirs) = find_current_dir_index(&lines); + if !on_dirs { + log::debug!("build_from_reader: current directory not found in siblings"); } Dirs { - entries: dirs, - parent: PathBuf::from(base), + entries: lines, + parent: PathBuf::from(parent), + on_dirs, current, } } -fn find_current_dir_index(dirs: &[PathBuf]) -> usize { +fn not_exists(path: &Path) -> bool { + if path.exists() { + false + } else { + let sibling_p = Path::new("..").join(path); + !sibling_p.exists() + } +} + +fn find_current_dir_index(dirs: &[PathBuf]) -> (usize, bool) { log::trace!("find_current_dir_index(dirs.len={})", dirs.len()); if let Ok(pwd) = std::env::current_dir() { let cwd = PathBuf::from("."); @@ -375,16 +392,16 @@ fn find_current_dir_index(dirs: &[PathBuf]) -> usize { .iter() .position(|dir| dir == &cwd || pwd.ends_with(dir)) { - return pos; + return (pos, true); } } - 0 + (0, false) } -fn build_from_list(filename: &Path) -> Result { +fn build_from_list(filename: &Path, all_target: bool) -> Result { if let Ok(f) = std::fs::File::open(filename) { let reader = BufReader::new(f); - Ok(build_from_reader(Box::new(reader))) + Ok(build_from_reader(Box::new(reader), all_target)) } else { log::error!("build_from_list: I/O error: {}", filename.display()); Err(Error::Io(std::io::Error::last_os_error())) @@ -548,7 +565,7 @@ mod tests { let dirs = dirs.unwrap(); assert_eq!(dirs.len(), 4); assert_eq!(dirs.current, 1); - assert_eq!(dirs.parent, PathBuf::from("testdata/basic")); + assert_eq!(dirs.parent, PathBuf::from("../testdata/basic")); } #[test] diff --git a/lib/tests/common/mod.rs b/lib/tests/common/mod.rs new file mode 100644 index 0000000..0750bc4 --- /dev/null +++ b/lib/tests/common/mod.rs @@ -0,0 +1,8 @@ +pub(super) fn init() { + if std::env::var_os("RUST_LOG").is_none() { + unsafe { + std::env::set_var("RUST_LOG", "debug"); + } + } + let _ = env_logger::builder().is_test(true).try_init(); +} \ No newline at end of file diff --git a/lib/tests/skip_non_existent_dir.rs b/lib/tests/skip_non_existent_dir.rs new file mode 100644 index 0000000..f298c12 --- /dev/null +++ b/lib/tests/skip_non_existent_dir.rs @@ -0,0 +1,31 @@ +use std::path::Path; + +mod common; + +#[test] +pub fn test_skip_non_existent_dir() { + common::init(); + let _ = std::env::set_current_dir(Path::new("../testdata/basic")); + let dirs = sibling::Dirs::new_from_file("../skip.txt") + .expect("Failed to create Dirs from skip.txt"); + assert_eq!(dirs.len(), 2); + + let dir = dirs.next( + sibling::NexterFactory::create(sibling::NexterType::Next).as_ref(), + ).expect("Failed to get next directory"); + assert_eq!(dir.path().to_string_lossy(), "worried"); +} + +#[test] +pub fn test_skip_non_existent_dir2() { + common::init(); + let _ = std::env::set_current_dir(Path::new("../testdata/basic")); + let dirs = sibling::Dirs::new_from_file_with("../skip.txt", true) + .expect("Failed to create Dirs from skip.txt"); + assert_eq!(dirs.len(), 4); + + let dir = dirs.next( + sibling::NexterFactory::create(sibling::NexterType::Previous).as_ref(), + ).expect("Failed to get next directory"); + assert_eq!(dir.path().to_string_lossy(), "unknown_dir"); +} diff --git a/testdata/basic/dirlist.txt b/testdata/basic/dirlist.txt index c03f276..6816ab5 100644 --- a/testdata/basic/dirlist.txt +++ b/testdata/basic/dirlist.txt @@ -1,5 +1,5 @@ -testdata/basic/a +../testdata/basic/a . -testdata/basic/b -testdata/basic/c -parent:testdata/basic +../testdata/basic/b +../testdata/basic/c +parent:../testdata/basic diff --git a/testdata/demo/.init_demo.sh b/testdata/demo/.init_demo.sh new file mode 100644 index 0000000..923c2d5 --- /dev/null +++ b/testdata/demo/.init_demo.sh @@ -0,0 +1,2 @@ +LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | fold -w 8 | head -n 100 | xargs mkdir +\ls | shuf | head -5 > target.txt diff --git a/testdata/demo/target.txt b/testdata/demo/target.txt new file mode 100644 index 0000000..cd542bb --- /dev/null +++ b/testdata/demo/target.txt @@ -0,0 +1,5 @@ +ZY5ByfDz +g2md2Rj2 +D6Wwt9gT +YZHpDTsi +rsIXWSba diff --git a/testdata/skip.txt b/testdata/skip.txt new file mode 100644 index 0000000..fbcf9fa --- /dev/null +++ b/testdata/skip.txt @@ -0,0 +1,4 @@ +unknown_dir +basic +worried +non_exists From 40997ebaf828081bd3cc2a0bea7d64953a1eb591 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 13:17:39 +0900 Subject: [PATCH 09/19] refactor: enhance CLI options and logging initialization for improved usability --- cli/src/cli.rs | 69 +++++++++++++++++++++++++++++++++++++++------- cli/src/main.rs | 29 ++----------------- cli/src/minisib.rs | 8 +++--- lib/src/lib.rs | 37 ++++++++++++------------- 4 files changed, 84 insertions(+), 59 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 7d63c5b..d1a3b85 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -13,6 +13,43 @@ pub struct CliOpts { #[clap(flatten)] pub(crate) nexter_opts: NexterOpts, + #[clap(flatten)] + pub(crate) init_script: InitOpts, + + #[clap(flatten)] + pub(crate) log_opts: LogOpts, + + #[arg(short = 'w', long = "working-dir", help = "set the current working directory.", hide = true, value_name = "DIR", + long_help = "This option is applied before any other processing. Therefore, other options that specify paths should use the relative path from this option value. If this option is not specified, the current directory is used.")] + pub(crate) cwd: Option, + + #[arg(index = 1, help = "the target directory", value_name = "DIR")] + pub dirs: Vec, + + #[cfg(debug_assertions)] + #[clap(flatten)] + pub(crate) compopts: CompletionOpts, + + #[clap(subcommand)] + pub(crate) minisib: Option, +} + +impl CliOpts { + pub fn init(&mut self) { + self.log_opts.init(); + if let Some(cwd) = &self.cwd { + if let Err(e) = std::env::set_current_dir(cwd) { + log::error!("Failed to set current directory to {:?}: {}, use \".\"", cwd, e); + } + } + if self.dirs.is_empty() { + self.dirs.push(std::env::current_dir().unwrap()); + } + } +} + +#[derive(Parser, Debug)] +pub(crate) struct LogOpts { #[arg( long, help = "set the log level", @@ -22,7 +59,29 @@ pub struct CliOpts { ignore_case = true )] pub log: LogLevel, +} + +impl LogOpts { + pub fn init(&self) { + use LogLevel::{Error, Warn, Info, Debug, Trace}; + if std::env::var_os("RUST_LOG").is_none() { + unsafe { + match self.log { + Error => std::env::set_var("RUST_LOG", "error"), + Warn => std::env::set_var("RUST_LOG", "warn"), + Info => std::env::set_var("RUST_LOG", "info"), + Debug => std::env::set_var("RUST_LOG", "debug"), + Trace => std::env::set_var("RUST_LOG", "trace"), + }; + } + } + env_logger::init(); + log::info!("Log level set to {:?}", self.log); + } +} +#[derive(Parser, Debug)] +pub(crate) struct InitOpts { #[arg( long, help = "generate the initialize script for the shell", @@ -31,16 +90,6 @@ pub struct CliOpts { default_missing_value = "bash" )] pub init: Option, - - #[arg(index = 1, help = "the target directory", value_name = "DIR")] - pub dirs: Vec, - - #[cfg(debug_assertions)] - #[clap(flatten)] - pub(crate) compopts: CompletionOpts, - - #[clap(subcommand)] - pub(crate) minisib: Option, } #[derive(Parser, Debug)] diff --git a/cli/src/main.rs b/cli/src/main.rs index d266108..914116c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -19,23 +19,6 @@ pub enum LogLevel { Trace, } -fn init_log(level: &LogLevel) { - use LogLevel::{Error, Warn, Info, Debug, Trace}; - if std::env::var_os("RUST_LOG").is_none() { - unsafe { - match level { - Error => std::env::set_var("RUST_LOG", "error"), - Warn => std::env::set_var("RUST_LOG", "warn"), - Info => std::env::set_var("RUST_LOG", "info"), - Debug => std::env::set_var("RUST_LOG", "debug"), - Trace => std::env::set_var("RUST_LOG", "trace"), - }; - } - } - env_logger::init(); - log::info!("Log level set to {level:?}"); -} - fn perform_impl( dirs: &Dirs, nexter: &dyn Nexter, @@ -91,7 +74,7 @@ fn perform_sibling(opts: CliOpts) -> Vec> { } fn perform(opts: CliOpts) -> Vec> { - if let Some(shell) = opts.init { + if let Some(shell) = opts.init_script.init { vec![init::generate_init_script(&shell)] } else if opts.nexter_opts.input.is_some() { perform_from_file(opts) @@ -103,14 +86,8 @@ fn perform(opts: CliOpts) -> Vec> { } fn main() { - let mut args = std::env::args(); - let args = if args.len() == 1 { - vec![args.next().unwrap(), ".".into()] - } else { - args.collect() - }; - let opts = cli::CliOpts::parse_from(args); - init_log(&opts.log); + let mut opts = cli::CliOpts::parse(); + opts.init(); if cfg!(debug_assertions) { #[cfg(debug_assertions)] if opts.compopts.completion { diff --git a/cli/src/minisib.rs b/cli/src/minisib.rs index 3de2f93..3f5f6eb 100644 --- a/cli/src/minisib.rs +++ b/cli/src/minisib.rs @@ -39,10 +39,10 @@ impl RawOpts { if self.args.is_empty() { Err(Error::Fatal("NEXTER_TYPE is required".into())) } else if self.args.len() == 1 { - let nexter = NexterType::from(self.args[0].as_str()); + let nexter = self.args.first().unwrap().parse::()?; Ok(MiniSibOpts::new(nexter, 1, None)) } else if self.args.len() == 2 { - let nexter = NexterType::from(self.args.first().unwrap().as_str()); + let nexter = self.args.first().unwrap().parse::()?; if let Ok(n) = self.args[1].parse::() { let step = n; Ok(MiniSibOpts::new(nexter, step, None)) @@ -51,7 +51,7 @@ impl RawOpts { Ok(MiniSibOpts::new(nexter, 1, Some(file))) } } else { - let nexter = NexterType::from(self.args.first().unwrap().as_str()); + let nexter = self.args.first().unwrap().parse::()?; if let Ok(n) = self.args[1].parse::() { let step = n; let file = Some(self.args[2].as_str().to_string()); @@ -93,7 +93,7 @@ impl MiniSibOpts { Ok(format!("{}\n{}\n{}\n{}", crate::printer::pathbuf_to_string(Some(dir.path()), false, target_dirs.on_dirs), target_dirs.len(), - dir.index(), + dir.index() + 1, dir.is_last_item())) } else { Ok(format!("{}\n{}\n{}\n{}", diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 96d1e7f..36338a6 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -16,6 +16,7 @@ use std::fmt::Display; use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; +use std::str::FromStr; use clap::ValueEnum; @@ -39,29 +40,28 @@ pub enum NexterType { Keep, } -impl From<&str> for NexterType { - fn from(s: &str) -> Self { +impl FromStr for NexterType { + type Err = Error; + fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { - "first" => NexterType::First, - "last" => NexterType::Last, - "previous" => NexterType::Previous, - "next" => NexterType::Next, - "random" => NexterType::Random, - "keep" => NexterType::Keep, - _ => NexterType::Next, + "first" => Ok(NexterType::First), + "last" => Ok(NexterType::Last), + "previous" => Ok(NexterType::Previous), + "next" => Ok(NexterType::Next), + "random" => Ok(NexterType::Random), + "keep" => Ok(NexterType::Keep), + _ => Err(Error::UnknownNexterType(s.to_string())), } } } -impl From for NexterType { - fn from(s: String) -> Self { - NexterType::from(s.as_str()) - } -} - /// The error type for sibling. #[derive(Debug)] pub enum Error { + /// Multiple errors occurred (array of errors). + Array(Vec), + /// A fatal error with a custom message. + Fatal(String), /// I/O error occurred while accessing the file system. Io(std::io::Error), /// The specified path was not found. @@ -72,10 +72,8 @@ pub enum Error { NotDir(PathBuf), /// The specified path is not a file. NotFile(PathBuf), - /// A fatal error with a custom message. - Fatal(String), - /// Multiple errors occurred (array of errors). - Array(Vec), + /// The specified nexter type is unknown. + UnknownNexterType(String), } impl Display for Error { @@ -93,6 +91,7 @@ impl Display for Error { Error::NotFile(path) => write!(f, "{}: Not a file", path.display()), Error::NotFound(path) => write!(f, "{}: Not found", path.display()), Error::Fatal(message) => write!(f, "Fatal error: {message}"), + Error::UnknownNexterType(s) => write!(f, "Unknown nexter type: {s}"), } } } From c5cece206119bb4c8bdae25eb34e1bc906ee3285 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 13:22:50 +0900 Subject: [PATCH 10/19] refactor: improve argument validation in RawOpts parser for better error handling --- cli/src/minisib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/src/minisib.rs b/cli/src/minisib.rs index 3f5f6eb..d2328d8 100644 --- a/cli/src/minisib.rs +++ b/cli/src/minisib.rs @@ -38,6 +38,8 @@ impl RawOpts { pub(crate) fn parse(&self) -> Result { if self.args.is_empty() { Err(Error::Fatal("NEXTER_TYPE is required".into())) + } else if self.args.len() > 3 { + Err(Error::Fatal("Too many arguments".into())) } else if self.args.len() == 1 { let nexter = self.args.first().unwrap().parse::()?; Ok(MiniSibOpts::new(nexter, 1, None)) From 2612c24e6005f91f72f1f341a2072dfa640a01d7 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 13:29:14 +0900 Subject: [PATCH 11/19] refactor: streamline error logging and simplify path handling in CLI and library --- cli/src/cli.rs | 2 +- cli/src/printer.rs | 8 +++----- lib/src/lib.rs | 20 +++++++++----------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index d1a3b85..938ca72 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -39,7 +39,7 @@ impl CliOpts { self.log_opts.init(); if let Some(cwd) = &self.cwd { if let Err(e) = std::env::set_current_dir(cwd) { - log::error!("Failed to set current directory to {:?}: {}, use \".\"", cwd, e); + log::error!("Failed to set current directory to {cwd:?}: {e}, use \".\""); } } if self.dirs.is_empty() { diff --git a/cli/src/printer.rs b/cli/src/printer.rs index ae7078b..3369e96 100644 --- a/cli/src/printer.rs +++ b/cli/src/printer.rs @@ -98,12 +98,10 @@ pub(crate) fn pathbuf_to_string(path: Option<&Path>, absolute: bool, on_dirs: bo .unwrap() .to_string_lossy() .to_string() + } else if on_dirs && !p.is_absolute() { + format!("../{}", p.to_string_lossy()) } else { - if on_dirs && !p.is_absolute() { - format!("../{}", p.to_string_lossy()) - } else { - p.to_string_lossy().to_string() - } + p.to_string_lossy().to_string() } } None => String::new(), diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 36338a6..35328db 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -294,7 +294,7 @@ fn build_dirs(parent: Option<&Path>, current: PathBuf) -> Result { Ok(Dirs { entries: dirs, parent: parent.to_path_buf(), - on_dirs: on_dirs, + on_dirs, current: index, }) } else { @@ -347,17 +347,15 @@ fn find_current(dirs: &[PathBuf], current: &PathBuf) -> (usize, bool) { fn build_from_reader(reader: Box, all_target: bool) -> Dirs { let mut parent = String::from("."); let mut lines = vec![]; - for line in reader.lines() { - if let Ok(line) = line { - if line.starts_with("parent:") { - parent = line.chars().skip("parent:".len()).collect::().trim().to_string(); + for line in reader.lines().map_while(|r| r.ok()) { + if line.starts_with("parent:") { + parent = line.chars().skip("parent:".len()).collect::().trim().to_string(); + } else { + let p = Path::new(line.trim()); + if !all_target && not_exists(p) { + continue; } else { - let p = Path::new(line.trim()); - if !all_target && not_exists(p) { - continue; - } else { - lines.push(p.to_path_buf()); - } + lines.push(p.to_path_buf()); } } } From 2158f2202bea7e17fd31d93d644cc932de95318a Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 13:54:55 +0900 Subject: [PATCH 12/19] refactor: improve code formatting and organization across multiple files for better readability --- cli/src/cli.rs | 19 +++++++++--- cli/src/gencomp.rs | 16 +++++++++-- cli/src/main.rs | 16 +++++------ cli/src/minisib.rs | 46 +++++++++++++++++------------- cli/src/printer.rs | 12 ++++---- lib/src/lib.rs | 44 +++++++++++++++++++--------- lib/tests/common/mod.rs | 2 +- lib/tests/skip_non_existent_dir.rs | 16 +++++------ 8 files changed, 108 insertions(+), 63 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 938ca72..581d926 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -19,8 +19,14 @@ pub struct CliOpts { #[clap(flatten)] pub(crate) log_opts: LogOpts, - #[arg(short = 'w', long = "working-dir", help = "set the current working directory.", hide = true, value_name = "DIR", - long_help = "This option is applied before any other processing. Therefore, other options that specify paths should use the relative path from this option value. If this option is not specified, the current directory is used.")] + #[arg( + short = 'w', + long = "working-dir", + help = "set the current working directory.", + hide = true, + value_name = "DIR", + long_help = "This option is applied before any other processing. Therefore, other options that specify paths should use the relative path from this option value. If this option is not specified, the current directory is used." + )] pub(crate) cwd: Option, #[arg(index = 1, help = "the target directory", value_name = "DIR")] @@ -63,7 +69,7 @@ pub(crate) struct LogOpts { impl LogOpts { pub fn init(&self) { - use LogLevel::{Error, Warn, Info, Debug, Trace}; + use LogLevel::{Debug, Error, Info, Trace, Warn}; if std::env::var_os("RUST_LOG").is_none() { unsafe { match self.log { @@ -114,7 +120,12 @@ pub(crate) struct NexterOpts { )] pub input: Option, - #[arg(short = 'a', long, help = "Set the targets to all directories from the given list. By default, the sibling skips non-existent directories.", default_value_t = false)] + #[arg( + short = 'a', + long, + help = "Set the targets to all directories from the given list. By default, the sibling skips non-existent directories.", + default_value_t = false + )] pub all: bool, } diff --git a/cli/src/gencomp.rs b/cli/src/gencomp.rs index 8915f35..7288bd9 100644 --- a/cli/src/gencomp.rs +++ b/cli/src/gencomp.rs @@ -24,9 +24,21 @@ mod generator { app.set_bin_name(appname); generate_impl(Bash, &mut app, appname, outdir, format!("bash/{appname}")); - generate_impl(Elvish, &mut app, appname, outdir, format!("elvish/{appname}")); + generate_impl( + Elvish, + &mut app, + appname, + outdir, + format!("elvish/{appname}"), + ); generate_impl(Fish, &mut app, appname, outdir, format!("fish/{appname}")); - generate_impl(PowerShell, &mut app, appname, outdir, format!("powershell/{appname}")); + generate_impl( + PowerShell, + &mut app, + appname, + outdir, + format!("powershell/{appname}"), + ); generate_impl(Zsh, &mut app, appname, outdir, format!("zsh/_{appname}")); } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 914116c..2eb9de7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -7,8 +7,8 @@ use sibling::{Dirs, Error, Nexter, Result}; mod cli; mod gencomp; mod init; -pub(crate) mod printer; pub(crate) mod minisib; +pub(crate) mod printer; #[derive(clap::ValueEnum, Clone, Debug)] pub enum LogLevel { @@ -19,12 +19,7 @@ pub enum LogLevel { Trace, } -fn perform_impl( - dirs: &Dirs, - nexter: &dyn Nexter, - step: i32, - opts: &PrintingOpts, -) -> String { +fn perform_impl(dirs: &Dirs, nexter: &dyn Nexter, step: i32, opts: &PrintingOpts) -> String { let next = dirs.next_with(nexter, step); printer::result_string(dirs, next, opts) } @@ -35,7 +30,12 @@ fn perform_from_file(opts: CliOpts) -> Vec> { None => Err(Error::Fatal("input is not specified".into())), Some(file) => match Dirs::new_from_file_with(file, opts.nexter_opts.all) { Err(e) => Err(e), - Ok(dirs) => Ok(perform_impl(&dirs, nexter.as_ref(), opts.nexter_opts.step, &opts.p_opts)), + Ok(dirs) => Ok(perform_impl( + &dirs, + nexter.as_ref(), + opts.nexter_opts.step, + &opts.p_opts, + )), }, }; vec![r] diff --git a/cli/src/minisib.rs b/cli/src/minisib.rs index d2328d8..d84a5f5 100644 --- a/cli/src/minisib.rs +++ b/cli/src/minisib.rs @@ -1,7 +1,7 @@ //! minisib: A minimal sibling command implementation -//! +//! //! This program is used for directory traversing in the shell scripts. -//! +//! //! Usage: //! minisib [OPTIONS] [INPUT_FILE] //! OPTIONS: @@ -9,21 +9,23 @@ //! -1 means minus one step, -2 means minus two steps, and +2 means plus two steps. //! NEXTER_TYPE: next, previous, first, last, random, keep //! INPUT_FILE: file containing the list of directories (if not provided, uses current directory +use clap::{Parser, Subcommand}; use sibling::{Dirs, Error, NexterType, Result}; -use clap::{Subcommand, Parser}; #[derive(Subcommand, Debug)] pub(crate) enum MiniSibCommand { - #[command(name = "minisib", about = "Great Finding! A minimal sibling command implementation. This command is a helper utility for the shell. It assumes the user will not use it.", hide = true)] + #[command( + name = "minisib", + about = "Great Finding! A minimal sibling command implementation. This command is a helper utility for the shell. It assumes the user will not use it.", + hide = true + )] MiniSib(RawOpts), } impl MiniSibCommand { pub(crate) fn perform(&self) -> Result { match self { - MiniSibCommand::MiniSib(opts) => { - opts.parse().and_then(|item| item.perform()) - } + MiniSibCommand::MiniSib(opts) => opts.parse().and_then(|item| item.perform()), } } } @@ -92,19 +94,26 @@ impl MiniSibOpts { Dirs::new(std::env::current_dir().unwrap())? }; if let Some(dir) = target_dirs.next_with(nexter.as_ref(), self.step) { - Ok(format!("{}\n{}\n{}\n{}", + Ok(format!( + "{}\n{}\n{}\n{}", crate::printer::pathbuf_to_string(Some(dir.path()), false, target_dirs.on_dirs), target_dirs.len(), dir.index() + 1, - dir.is_last_item())) + dir.is_last_item() + )) } else { - Ok(format!("{}\n{}\n{}\n{}", - crate::printer::pathbuf_to_string(Some(target_dirs.parent()), false, target_dirs.on_dirs), + Ok(format!( + "{}\n{}\n{}\n{}", + crate::printer::pathbuf_to_string( + Some(target_dirs.parent()), + false, + target_dirs.on_dirs + ), target_dirs.len(), -1, - true)) + true + )) } - } } @@ -117,8 +126,7 @@ mod tests { #[test] fn test_args_1() { let args = vec!["sibling", "minisib", "previous", "-2", "dirs.txt"]; - let minisib = crate::cli::CliOpts::parse_from(&args) - .minisib; + let minisib = crate::cli::CliOpts::parse_from(&args).minisib; assert!(minisib.is_some()); let opts = if let Some(MiniSibCommand::MiniSib(opts)) = minisib { opts.parse().unwrap() @@ -134,8 +142,7 @@ mod tests { #[test] fn test_args_2() { let args = vec!["sibling", "minisib", "previous", "dirs.txt"]; - let minisib = crate::cli::CliOpts::parse_from(&args) - .minisib; + let minisib = crate::cli::CliOpts::parse_from(&args).minisib; assert!(minisib.is_some()); let opts = if let Some(MiniSibCommand::MiniSib(opts)) = minisib { opts.parse().unwrap() @@ -150,8 +157,7 @@ mod tests { #[test] fn test_args_3() { let args = vec!["sibling", "minisib", "keep"]; - let minisib = crate::cli::CliOpts::parse_from(&args) - .minisib; + let minisib = crate::cli::CliOpts::parse_from(&args).minisib; assert!(minisib.is_some()); let opts = if let Some(MiniSibCommand::MiniSib(opts)) = minisib { opts.parse().unwrap() @@ -162,4 +168,4 @@ mod tests { assert_eq!(opts.nexter_type, super::NexterType::Keep); assert!(opts.file.is_none()); } -} \ No newline at end of file +} diff --git a/cli/src/printer.rs b/cli/src/printer.rs index 3369e96..d6503d4 100644 --- a/cli/src/printer.rs +++ b/cli/src/printer.rs @@ -3,11 +3,7 @@ use std::path::Path; use crate::cli::{Format, PrintingOpts}; use sibling::{Dir, Dirs}; -pub(crate) fn result_string( - dirs: &Dirs, - next: Option>, - opts: &PrintingOpts, -) -> String { +pub(crate) fn result_string(dirs: &Dirs, next: Option>, opts: &PrintingOpts) -> String { match opts.format { Format::Json => json_string(dirs, next, opts.absolute), Format::Csv => csv_string(dirs, next, opts.absolute), @@ -18,13 +14,15 @@ pub(crate) fn result_string( } else { result_string_impl(dirs, next, opts) } - }, + } } } fn json_string(dirs: &Dirs, next: Option>, absolute: bool) -> String { let current = dirs.current(); - let next_path = next.as_ref().map(|n| pathbuf_to_string(Some(n.path()), absolute, dirs.on_dirs)); + let next_path = next + .as_ref() + .map(|n| pathbuf_to_string(Some(n.path()), absolute, dirs.on_dirs)); format!( r#"{{"current":{{"path":"{}","index":{}}},"next":{{"path":"{}","index":{}}},"total":{}}}"#, pathbuf_to_string(Some(dirs.current().path()), absolute, dirs.on_dirs), diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 35328db..36b518b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -220,11 +220,18 @@ impl Dirs { let file = file.as_ref(); if file == "-" { log::info!("Reading directories from stdin"); - return Ok(build_from_reader(Box::new(std::io::stdin().lock()), all_target)); + return Ok(build_from_reader( + Box::new(std::io::stdin().lock()), + all_target, + )); } let path = PathBuf::from(file); if !path.exists() { - log::error!("Dirs::new_from_file: Not found: {}, pwd: {}", path.display(), std::env::current_dir().unwrap().display()); + log::error!( + "Dirs::new_from_file: Not found: {}, pwd: {}", + path.display(), + std::env::current_dir().unwrap().display() + ); Err(Error::NotFound(path)) } else if path.is_dir() { log::error!("Dirs::new_from_file: Not a file: {}", path.display()); @@ -281,8 +288,11 @@ impl Dirs { /// Collect sibling directories under parent and compute the index of current. fn build_dirs(parent: Option<&Path>, current: PathBuf) -> Result { - log::trace!("build_dirs(parent={parent:?}, current={})", current.display()); - let Some(parent) = parent else { + log::trace!( + "build_dirs(parent={parent:?}, current={})", + current.display() + ); + let Some(parent) = parent else { log::error!("build_dirs: No parent for current={}", current.display()); return Err(Error::NoParent(current)); }; @@ -331,9 +341,7 @@ fn collect_dirs(parent: &Path, errs: &mut Vec) -> Vec { /// Return the index of current in dirs, or 0 if not found. fn find_current(dirs: &[PathBuf], current: &PathBuf) -> (usize, bool) { - let idx = dirs - .iter() - .position(|dir| dir == current); + let idx = dirs.iter().position(|dir| dir == current); if let Some(index) = idx { log::trace!("find_current: found current at index {index}"); (index, true) @@ -349,7 +357,12 @@ fn build_from_reader(reader: Box, all_target: bool) -> Dirs { let mut lines = vec![]; for line in reader.lines().map_while(|r| r.ok()) { if line.starts_with("parent:") { - parent = line.chars().skip("parent:".len()).collect::().trim().to_string(); + parent = line + .chars() + .skip("parent:".len()) + .collect::() + .trim() + .to_string(); } else { let p = Path::new(line.trim()); if !all_target && not_exists(p) { @@ -359,7 +372,11 @@ fn build_from_reader(reader: Box, all_target: bool) -> Dirs { } } } - log::debug!("build_from_reader: base='{}', entries={}", parent, lines.len()); + log::debug!( + "build_from_reader: base='{}', entries={}", + parent, + lines.len() + ); let (current, on_dirs) = find_current_dir_index(&lines); if !on_dirs { log::debug!("build_from_reader: current directory not found in siblings"); @@ -499,14 +516,15 @@ fn next_impl(dirs: &Dirs, step: i32) -> Option> { dirs.current ); if next < 0 || next >= length { - log::warn!( - "next_impl: out of range (next={next}, len={length})", - ); + log::warn!("next_impl: out of range (next={next}, len={length})",); None } else if next == 0 { Some(Dir::new_of_last_item(dirs, 0)) } else if next == length - 1 { - Some(Dir::new_of_last_item(dirs, usize::try_from(length - 1).unwrap())) + Some(Dir::new_of_last_item( + dirs, + usize::try_from(length - 1).unwrap(), + )) } else { Some(Dir::new(dirs, usize::try_from(next).unwrap())) } diff --git a/lib/tests/common/mod.rs b/lib/tests/common/mod.rs index 0750bc4..013be36 100644 --- a/lib/tests/common/mod.rs +++ b/lib/tests/common/mod.rs @@ -5,4 +5,4 @@ pub(super) fn init() { } } let _ = env_logger::builder().is_test(true).try_init(); -} \ No newline at end of file +} diff --git a/lib/tests/skip_non_existent_dir.rs b/lib/tests/skip_non_existent_dir.rs index f298c12..dd45b24 100644 --- a/lib/tests/skip_non_existent_dir.rs +++ b/lib/tests/skip_non_existent_dir.rs @@ -6,13 +6,13 @@ mod common; pub fn test_skip_non_existent_dir() { common::init(); let _ = std::env::set_current_dir(Path::new("../testdata/basic")); - let dirs = sibling::Dirs::new_from_file("../skip.txt") - .expect("Failed to create Dirs from skip.txt"); + let dirs = + sibling::Dirs::new_from_file("../skip.txt").expect("Failed to create Dirs from skip.txt"); assert_eq!(dirs.len(), 2); - let dir = dirs.next( - sibling::NexterFactory::create(sibling::NexterType::Next).as_ref(), - ).expect("Failed to get next directory"); + let dir = dirs + .next(sibling::NexterFactory::create(sibling::NexterType::Next).as_ref()) + .expect("Failed to get next directory"); assert_eq!(dir.path().to_string_lossy(), "worried"); } @@ -24,8 +24,8 @@ pub fn test_skip_non_existent_dir2() { .expect("Failed to create Dirs from skip.txt"); assert_eq!(dirs.len(), 4); - let dir = dirs.next( - sibling::NexterFactory::create(sibling::NexterType::Previous).as_ref(), - ).expect("Failed to get next directory"); + let dir = dirs + .next(sibling::NexterFactory::create(sibling::NexterType::Previous).as_ref()) + .expect("Failed to get next directory"); assert_eq!(dir.path().to_string_lossy(), "unknown_dir"); } From 07efe2a40f61d5f8398c925d244d75e54058d66e Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 13:57:32 +0900 Subject: [PATCH 13/19] refactor: simplify current directory handling in CliOpts initialization --- cli/src/cli.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 581d926..a13dc19 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -43,10 +43,8 @@ pub struct CliOpts { impl CliOpts { pub fn init(&mut self) { self.log_opts.init(); - if let Some(cwd) = &self.cwd { - if let Err(e) = std::env::set_current_dir(cwd) { - log::error!("Failed to set current directory to {cwd:?}: {e}, use \".\""); - } + if let Some(cwd) = &self.cwd && let Err(e) = std::env::set_current_dir(cwd) { + log::error!("Failed to set current directory to {cwd:?}: {e}, use \".\""); } if self.dirs.is_empty() { self.dirs.push(std::env::current_dir().unwrap()); From 9db6d8f26991f943a999c3f10a7f75cf5507f503 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 14:44:38 +0900 Subject: [PATCH 14/19] refactor: improve directory change handling and error logging in path resolution --- assets/init/init.bash | 2 +- cli/src/printer.rs | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/assets/init/init.bash b/assets/init/init.bash index f365d67..31f2a41 100644 --- a/assets/init/init.bash +++ b/assets/init/init.bash @@ -15,7 +15,7 @@ __change_directory_with_sibling() { # echo "Current: $current, Next: $next, ci: $ci, ni: $ni, total: $total" if [[ $sibling_status -eq 0 ]] ; then # strip the first and last double quotes - cd $next + cd -- "$next" echo "$PWD (${current}/${length})" else echo "Done (${current}/${length})" diff --git a/cli/src/printer.rs b/cli/src/printer.rs index d6503d4..0a95563 100644 --- a/cli/src/printer.rs +++ b/cli/src/printer.rs @@ -92,12 +92,15 @@ pub(crate) fn pathbuf_to_string(path: Option<&Path>, absolute: bool, on_dirs: bo match path { Some(p) => { if absolute { - std::fs::canonicalize(p) - .unwrap() - .to_string_lossy() - .to_string() + match std::fs::canonicalize(p) { + Ok(path) => path.to_string_lossy().to_string(), + Err(e) => { + log::error!("Failed to get canonical path for {p:?}: {e}"); + p.to_string_lossy().to_string() + }, + } } else if on_dirs && !p.is_absolute() { - format!("../{}", p.to_string_lossy()) + Path::new("..").join(p).to_string_lossy().to_string() } else { p.to_string_lossy().to_string() } From e567d38f0b12904e0ba4c50eb0d5a05496237f54 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 18:20:37 +0900 Subject: [PATCH 15/19] refactor: enhance error handling for current directory retrieval and improve documentation clarity --- cli/src/cli.rs | 9 ++++- cli/src/minisib.rs | 19 ++++++----- cli/src/printer.rs | 54 +++++++++++++++++++++++++----- lib/src/lib.rs | 28 +++++++++++----- lib/tests/skip_non_existent_dir.rs | 6 ++-- 5 files changed, 86 insertions(+), 30 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index a13dc19..5c4e5d3 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -47,7 +47,14 @@ impl CliOpts { log::error!("Failed to set current directory to {cwd:?}: {e}, use \".\""); } if self.dirs.is_empty() { - self.dirs.push(std::env::current_dir().unwrap()); + match std::env::current_dir() { + Ok(cwd) => self.dirs.push(cwd), + Err(e) => { + log::error!("Failed to get current directory: {e}"); + eprintln!("Error: failed to determine current working directory: {e}"); + std::process::exit(1); + } + } } } } diff --git a/cli/src/minisib.rs b/cli/src/minisib.rs index d84a5f5..c42a83a 100644 --- a/cli/src/minisib.rs +++ b/cli/src/minisib.rs @@ -3,12 +3,12 @@ //! This program is used for directory traversing in the shell scripts. //! //! Usage: -//! minisib [OPTIONS] [INPUT_FILE] -//! OPTIONS: -//! : specify the number of times to execute sibling (default: 1) -//! -1 means minus one step, -2 means minus two steps, and +2 means plus two steps. -//! NEXTER_TYPE: next, previous, first, last, random, keep -//! INPUT_FILE: file containing the list of directories (if not provided, uses current directory +//! minisib [NUM] [INPUT_FILE] +//! NEXTER_TYPE: next, previous, first, last, random, keep. +//! NUM: specify the number of times to execute sibling (default: 1) +//! -1 means minus one step, -2 means minus two steps, and +2 means plus two steps. +//! INPUT_FILE: file containing the list of directories (if not provided, uses current directory. +//! NUM and INPUT_FILE can be in any order. use clap::{Parser, Subcommand}; use sibling::{Dirs, Error, NexterType, Result}; @@ -91,12 +91,13 @@ impl MiniSibOpts { let target_dirs = if let Some(file) = &self.file { Dirs::new_from_file(file)? } else { - Dirs::new(std::env::current_dir().unwrap())? + let cwd = std::env::current_dir().map_err(Error::Io)?; + Dirs::new(cwd)? }; if let Some(dir) = target_dirs.next_with(nexter.as_ref(), self.step) { Ok(format!( "{}\n{}\n{}\n{}", - crate::printer::pathbuf_to_string(Some(dir.path()), false, target_dirs.on_dirs), + crate::printer::pathbuf_to_string(Some(dir.path()), false, target_dirs.on_dirs()), target_dirs.len(), dir.index() + 1, dir.is_last_item() @@ -107,7 +108,7 @@ impl MiniSibOpts { crate::printer::pathbuf_to_string( Some(target_dirs.parent()), false, - target_dirs.on_dirs + target_dirs.on_dirs() ), target_dirs.len(), -1, diff --git a/cli/src/printer.rs b/cli/src/printer.rs index 0a95563..c740559 100644 --- a/cli/src/printer.rs +++ b/cli/src/printer.rs @@ -22,10 +22,10 @@ fn json_string(dirs: &Dirs, next: Option>, absolute: bool) -> String { let current = dirs.current(); let next_path = next .as_ref() - .map(|n| pathbuf_to_string(Some(n.path()), absolute, dirs.on_dirs)); + .map(|n| path_to_string(Some(n.path()), absolute, dirs.on_dirs())); format!( r#"{{"current":{{"path":"{}","index":{}}},"next":{{"path":"{}","index":{}}},"total":{}}}"#, - pathbuf_to_string(Some(dirs.current().path()), absolute, dirs.on_dirs), + path_to_string(Some(dirs.current().path()), absolute, dirs.on_dirs()), current.index() + 1, next_path.unwrap_or_default(), next.map_or(-1, |n| i32::try_from(n.index()).unwrap() + 1), @@ -37,8 +37,8 @@ fn csv_string(dirs: &Dirs, next: Option>, absolute: bool) -> String { let current = dirs.current(); format!( r#""{}","{}",{},{},{}"#, - pathbuf_to_string(Some(dirs.current().path()), absolute, dirs.on_dirs), - pathbuf_to_string(next.as_ref().map(Dir::path), absolute, dirs.on_dirs), + path_to_string(Some(dirs.current().path()), absolute, dirs.on_dirs()), + path_to_string(next.as_ref().map(Dir::path), absolute, dirs.on_dirs()), current.index() + 1, next.map_or(-1, |n| i32::try_from(n.index()).unwrap() + 1), dirs.len() @@ -47,7 +47,7 @@ fn csv_string(dirs: &Dirs, next: Option>, absolute: bool) -> String { fn no_more_dir_string(dirs: &Dirs, opts: &PrintingOpts) -> String { if opts.parent { - pathbuf_to_string(Some(dirs.parent()), opts.absolute, dirs.on_dirs) + path_to_string(Some(dirs.parent()), opts.absolute, dirs.on_dirs()) } else { String::from("no more sibling directory") } @@ -69,7 +69,7 @@ fn list_string(dirs: &Dirs, next: Option<&Dir<'_>>, opts: &PrintingOpts) -> Stri "{:>4} {}{}", i + 1, prefix, - pathbuf_to_string(Some(dir), opts.absolute, dirs.on_dirs) + path_to_string(Some(dir), opts.absolute, dirs.on_dirs()) )); } result.join("\n") @@ -79,16 +79,16 @@ fn result_string_impl(dirs: &Dirs, next: Option>, opts: &PrintingOpts) - if opts.progress { format!( "{} ({}/{})", - pathbuf_to_string(next.as_ref().map(Dir::path), opts.absolute, dirs.on_dirs), + path_to_string(next.as_ref().map(Dir::path), opts.absolute, dirs.on_dirs()), next.map_or(-1, |n| i32::try_from(n.index()).unwrap()) + 1, dirs.len() ) } else { - pathbuf_to_string(next.as_ref().map(Dir::path), opts.absolute, dirs.on_dirs).to_string() + path_to_string(next.as_ref().map(Dir::path), opts.absolute, dirs.on_dirs()).to_string() } } -pub(crate) fn pathbuf_to_string(path: Option<&Path>, absolute: bool, on_dirs: bool) -> String { +pub(crate) fn path_to_string(path: Option<&Path>, absolute: bool, on_dirs: bool) -> String { match path { Some(p) => { if absolute { @@ -108,3 +108,39 @@ pub(crate) fn pathbuf_to_string(path: Option<&Path>, absolute: bool, on_dirs: bo None => String::new(), } } + +#[cfg(test)] +mod tests { + use super::path_to_string; + use std::path::Path; + + #[test] + fn test_pathbuf_to_string_1() { + let p = Path::new("../testdata"); + let s = path_to_string(Some(p), false, true); + assert_eq!(s, "../testdata"); + } + + #[test] + fn test_pathbuf_to_string_2() { + let p = Path::new("../testdata"); + let s = path_to_string(Some(p), false, false); + assert_eq!(s, "testdata"); + } + + #[test] + fn test_pathbuf_to_string_3() { + let p = Path::new("../testdata"); + let s = path_to_string(Some(p), true, false); + let abs = std::fs::canonicalize(p).unwrap(); + assert_eq!(s, abs.to_string_lossy()); + } + + #[test] + fn test_pathbuf_to_string_4() { + let p = Path::new("../testdata"); + let s = path_to_string(Some(p), true, true); + let abs = std::fs::canonicalize(p).unwrap(); + assert_eq!(s, abs.to_string_lossy()); + } +} \ No newline at end of file diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 36b518b..571abdf 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -97,6 +97,7 @@ impl Display for Error { } /// Stores a list of sibling directories, the parent directory path, and the index of the current directory. +/// This struct manages the entries of sibling directories, parent directory, and the current directory index. #[derive(Debug, Clone)] pub struct Dirs { /// The list of sibling directories, sorted alphabetically. @@ -104,7 +105,7 @@ pub struct Dirs { /// The parent directory that contains all the sibling directories. parent: PathBuf, /// Flag indicating whether the current directory is among the sibling directories. - pub on_dirs: bool, + on_dirs: bool, /// The index of the current directory in the `dirs` vector. current: usize, } @@ -169,6 +170,8 @@ impl Dirs { /// # Returns /// /// A [`Result`]<[`Dirs`], [`Error`]> instance. + /// The resultant `Dirs` contains the list of sibling directories of the given path. + /// The parent directory is set to the parent of the given path. /// /// # Errors /// @@ -203,6 +206,7 @@ impl Dirs { /// Create a new [`Dirs`] instance from the given file. /// The file should contain a list of directories, one per line. /// The first line can optionally specify the parent directory in the format `parent:/path/to`. + /// The first character of the line is `#`, it is treated as a comment and ignored. /// If the current directory is not found in the list, the current index is set to 0. /// If `all_target` parameter is false, non-existent directories are skipped and /// the resultant list does not include them. @@ -237,7 +241,7 @@ impl Dirs { log::error!("Dirs::new_from_file: Not a file: {}", path.display()); Err(Error::NotFile(path)) } else { - build_from_list(&path, all_target) + build_from_list_file(&path, all_target) } } @@ -263,6 +267,10 @@ impl Dirs { self.entries.is_empty() } + pub fn on_dirs(&self) -> bool { + self.on_dirs + } + /// Get the number of directories in the list. #[must_use] pub fn len(&self) -> usize { @@ -353,16 +361,18 @@ fn find_current(dirs: &[PathBuf], current: &PathBuf) -> (usize, bool) { /// Parse lines from a reader; parent: sets base, remaining lines are directory entries. fn build_from_reader(reader: Box, all_target: bool) -> Dirs { - let mut parent = String::from("."); + let mut parent = None; let mut lines = vec![]; for line in reader.lines().map_while(|r| r.ok()) { if line.starts_with("parent:") { - parent = line + parent = Some(line .chars() .skip("parent:".len()) .collect::() .trim() - .to_string(); + .to_string()); + } else if line.starts_with("#") { + continue; } else { let p = Path::new(line.trim()); if !all_target && not_exists(p) { @@ -374,7 +384,7 @@ fn build_from_reader(reader: Box, all_target: bool) -> Dirs { } log::debug!( "build_from_reader: base='{}', entries={}", - parent, + parent.as_deref().unwrap_or(""), lines.len() ); let (current, on_dirs) = find_current_dir_index(&lines); @@ -383,7 +393,7 @@ fn build_from_reader(reader: Box, all_target: bool) -> Dirs { } Dirs { entries: lines, - parent: PathBuf::from(parent), + parent: PathBuf::from(parent.unwrap_or_else(|| if on_dirs { "..".to_string() } else { ".".to_string() })), on_dirs, current, } @@ -412,12 +422,12 @@ fn find_current_dir_index(dirs: &[PathBuf]) -> (usize, bool) { (0, false) } -fn build_from_list(filename: &Path, all_target: bool) -> Result { +fn build_from_list_file(filename: &Path, all_target: bool) -> Result { if let Ok(f) = std::fs::File::open(filename) { let reader = BufReader::new(f); Ok(build_from_reader(Box::new(reader), all_target)) } else { - log::error!("build_from_list: I/O error: {}", filename.display()); + log::error!("build_from_list_file: I/O error: {}", filename.display()); Err(Error::Io(std::io::Error::last_os_error())) } } diff --git a/lib/tests/skip_non_existent_dir.rs b/lib/tests/skip_non_existent_dir.rs index dd45b24..f831a92 100644 --- a/lib/tests/skip_non_existent_dir.rs +++ b/lib/tests/skip_non_existent_dir.rs @@ -5,7 +5,8 @@ mod common; #[test] pub fn test_skip_non_existent_dir() { common::init(); - let _ = std::env::set_current_dir(Path::new("../testdata/basic")); + std::env::set_current_dir(Path::new("../testdata/basic")) + .expect("Failed to change current directory"); let dirs = sibling::Dirs::new_from_file("../skip.txt").expect("Failed to create Dirs from skip.txt"); assert_eq!(dirs.len(), 2); @@ -19,7 +20,8 @@ pub fn test_skip_non_existent_dir() { #[test] pub fn test_skip_non_existent_dir2() { common::init(); - let _ = std::env::set_current_dir(Path::new("../testdata/basic")); + std::env::set_current_dir(Path::new("../testdata/basic")) + .expect("Failed to change current directory"); let dirs = sibling::Dirs::new_from_file_with("../skip.txt", true) .expect("Failed to create Dirs from skip.txt"); assert_eq!(dirs.len(), 4); From ab31e40ea55e0cb76529f7fe52b46e9c5ec11689 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 18:22:18 +0900 Subject: [PATCH 16/19] refactor: rename pathbuf_to_string to path_to_string for consistency in output formatting --- cli/src/minisib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/minisib.rs b/cli/src/minisib.rs index c42a83a..a581cce 100644 --- a/cli/src/minisib.rs +++ b/cli/src/minisib.rs @@ -97,7 +97,7 @@ impl MiniSibOpts { if let Some(dir) = target_dirs.next_with(nexter.as_ref(), self.step) { Ok(format!( "{}\n{}\n{}\n{}", - crate::printer::pathbuf_to_string(Some(dir.path()), false, target_dirs.on_dirs()), + crate::printer::path_to_string(Some(dir.path()), false, target_dirs.on_dirs()), target_dirs.len(), dir.index() + 1, dir.is_last_item() @@ -105,7 +105,7 @@ impl MiniSibOpts { } else { Ok(format!( "{}\n{}\n{}\n{}", - crate::printer::pathbuf_to_string( + crate::printer::path_to_string( Some(target_dirs.parent()), false, target_dirs.on_dirs() From 14652bdbcaf8b9eec5a1302c24fb81dd47cbfdee Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 18:28:33 +0900 Subject: [PATCH 17/19] refactor: update test assertions for path_to_string to reflect correct relative paths --- cli/src/printer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/printer.rs b/cli/src/printer.rs index c740559..b137a9c 100644 --- a/cli/src/printer.rs +++ b/cli/src/printer.rs @@ -118,14 +118,14 @@ mod tests { fn test_pathbuf_to_string_1() { let p = Path::new("../testdata"); let s = path_to_string(Some(p), false, true); - assert_eq!(s, "../testdata"); + assert_eq!(s, "../../testdata"); } #[test] fn test_pathbuf_to_string_2() { let p = Path::new("../testdata"); let s = path_to_string(Some(p), false, false); - assert_eq!(s, "testdata"); + assert_eq!(s, "../testdata"); } #[test] From 8541b60680b383dabb49aca8a90680ab7131b086 Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 19:37:43 +0900 Subject: [PATCH 18/19] refactor: improve error handling in current directory initialization and enhance documentation for minisib usage --- cli/src/cli.rs | 4 +++- cli/src/minisib.rs | 39 +++++++++++++++++++++++++++++++++++++- cli/src/printer.rs | 4 ++-- lib/src/lib.rs | 30 ++++++++++++++++++----------- testdata/basic/dirlist.txt | 8 +++----- 5 files changed, 65 insertions(+), 20 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 5c4e5d3..e161635 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -43,7 +43,9 @@ pub struct CliOpts { impl CliOpts { pub fn init(&mut self) { self.log_opts.init(); - if let Some(cwd) = &self.cwd && let Err(e) = std::env::set_current_dir(cwd) { + if let Some(cwd) = &self.cwd + && let Err(e) = std::env::set_current_dir(cwd) + { log::error!("Failed to set current directory to {cwd:?}: {e}, use \".\""); } if self.dirs.is_empty() { diff --git a/cli/src/minisib.rs b/cli/src/minisib.rs index a581cce..49f2733 100644 --- a/cli/src/minisib.rs +++ b/cli/src/minisib.rs @@ -2,13 +2,27 @@ //! //! This program is used for directory traversing in the shell scripts. //! -//! Usage: +//! ### Usage: +//! +//! ```text //! minisib [NUM] [INPUT_FILE] //! NEXTER_TYPE: next, previous, first, last, random, keep. //! NUM: specify the number of times to execute sibling (default: 1) //! -1 means minus one step, -2 means minus two steps, and +2 means plus two steps. //! INPUT_FILE: file containing the list of directories (if not provided, uses current directory. //! NUM and INPUT_FILE can be in any order. +//! ``` +//! +//! ### Output: +//! +//! The output of this subcommand consists of four lines: +//! +//! ```text +//! [NEXT_DIR_PATH or PARENT_DIR_PATH] +//! [TOTAL_NUMBER_OF_DIRECTORIES] +//! [INDEX_OF_NEXT_DIRECTORY (1-based, -1 if no next directory)] +//! [IS_LAST_ITEM (true/false)] +//! ``` use clap::{Parser, Subcommand}; use sibling::{Dirs, Error, NexterType, Result}; @@ -169,4 +183,27 @@ mod tests { assert_eq!(opts.nexter_type, super::NexterType::Keep); assert!(opts.file.is_none()); } + + #[test] + fn test_sibling() { + let args = vec![ + "sibling", + "--working-dir", + "../testdata/basic", + "minisib", + "first", + "1", + "dirlist.txt", + ]; + let mut cliopts = crate::cli::CliOpts::parse_from(&args); + cliopts.init(); + let result = cliopts.minisib.as_ref().unwrap().perform(); + assert!(result.is_ok()); + let output = result.unwrap(); + let lines: Vec<&str> = output.lines().collect(); + assert_eq!(lines[0], "a"); + assert_eq!(lines[1], "3"); + assert_eq!(lines[2], "1"); + assert_eq!(lines[3], "true"); + } } diff --git a/cli/src/printer.rs b/cli/src/printer.rs index b137a9c..99091e2 100644 --- a/cli/src/printer.rs +++ b/cli/src/printer.rs @@ -97,7 +97,7 @@ pub(crate) fn path_to_string(path: Option<&Path>, absolute: bool, on_dirs: bool) Err(e) => { log::error!("Failed to get canonical path for {p:?}: {e}"); p.to_string_lossy().to_string() - }, + } } } else if on_dirs && !p.is_absolute() { Path::new("..").join(p).to_string_lossy().to_string() @@ -143,4 +143,4 @@ mod tests { let abs = std::fs::canonicalize(p).unwrap(); assert_eq!(s, abs.to_string_lossy()); } -} \ No newline at end of file +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 571abdf..50e7862 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -365,12 +365,13 @@ fn build_from_reader(reader: Box, all_target: bool) -> Dirs { let mut lines = vec![]; for line in reader.lines().map_while(|r| r.ok()) { if line.starts_with("parent:") { - parent = Some(line - .chars() - .skip("parent:".len()) - .collect::() - .trim() - .to_string()); + parent = Some( + line.chars() + .skip("parent:".len()) + .collect::() + .trim() + .to_string(), + ); } else if line.starts_with("#") { continue; } else { @@ -393,7 +394,13 @@ fn build_from_reader(reader: Box, all_target: bool) -> Dirs { } Dirs { entries: lines, - parent: PathBuf::from(parent.unwrap_or_else(|| if on_dirs { "..".to_string() } else { ".".to_string() })), + parent: PathBuf::from(parent.unwrap_or_else(|| { + if on_dirs { + "..".to_string() + } else { + ".".to_string() + } + })), on_dirs, current, } @@ -585,12 +592,13 @@ mod tests { #[test] fn test_dir_from_file() { - let dirs = Dirs::new_from_file("../testdata/basic/dirlist.txt"); + let dirs = Dirs::new_from_file_with("../testdata/basic/dirlist.txt", true); assert!(dirs.is_ok()); let dirs = dirs.unwrap(); - assert_eq!(dirs.len(), 4); - assert_eq!(dirs.current, 1); - assert_eq!(dirs.parent, PathBuf::from("../testdata/basic")); + assert_eq!(dirs.len(), 3); + assert_eq!(dirs.current, 0); + assert!(!dirs.on_dirs()); + assert_eq!(dirs.parent, PathBuf::from(".")); } #[test] diff --git a/testdata/basic/dirlist.txt b/testdata/basic/dirlist.txt index 6816ab5..de98044 100644 --- a/testdata/basic/dirlist.txt +++ b/testdata/basic/dirlist.txt @@ -1,5 +1,3 @@ -../testdata/basic/a -. -../testdata/basic/b -../testdata/basic/c -parent:../testdata/basic +a +b +c From 537c8d02d28c66e7d45c673f2742fb378f044d6c Mon Sep 17 00:00:00 2001 From: Haruaki Tamada Date: Thu, 12 Feb 2026 20:40:42 +0900 Subject: [PATCH 19/19] refactor: add unit tests for error display in sibling operations --- cli/src/init.rs | 15 +++++++++++++++ lib/src/lib.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/cli/src/init.rs b/cli/src/init.rs index 018ef4b..92154d2 100644 --- a/cli/src/init.rs +++ b/cli/src/init.rs @@ -19,3 +19,18 @@ pub(crate) fn generate_init_script(shell_name: &str) -> Result { None => Err(Error::NotFound(script_file.into())), } } + +#[cfg(test)] +mod tests { + #[test] + fn test_generate_init_script_with_bash() { + let script = super::generate_init_script("bash"); + assert!(script.is_ok()); + } + + #[test] + fn test_generate_init_script_with_unsupported_shell() { + let script = super::generate_init_script("fish"); + assert!(script.is_err()); + } +} \ No newline at end of file diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 50e7862..8b38346 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -551,6 +551,51 @@ fn next_impl(dirs: &Dirs, step: i32) -> Option> { mod tests { use super::*; + #[test] + fn test_error_display_not_dir() { + let err = Error::NotDir(PathBuf::from("/path/to/file")); + assert_eq!( + format!("{}", err), + "/path/to/file: Not a directory".to_string() + ); + } + + #[test] + fn test_error_display_not_file() { + let err = Error::NotFile(PathBuf::from("/path/to/file")); + assert_eq!( + format!("{}", err), + "/path/to/file: Not a file".to_string() + ); + } + + #[test] + fn test_error_display_no_parent() { + let err = Error::NoParent(PathBuf::from("/path/to/file")); + assert_eq!( + format!("{}", err), + "/path/to/file: No parent directory".to_string() + ); + } + + #[test] + fn test_error_display_fatal() { + let err = Error::Fatal("Some fatal error".into()); + assert_eq!( + format!("{}", err), + "Fatal error: Some fatal error".to_string() + ); + } + + #[test] + fn test_error_display_unknown_nexter_type() { + let err = Error::UnknownNexterType("unknown".into()); + assert_eq!( + format!("{}", err), + "Unknown nexter type: unknown".to_string() + ); + } + #[test] fn test_dirs_new() { let dirs = Dirs::new(PathBuf::from("../testdata/basic/d"));