Compare commits

..

64 Commits

Author SHA1 Message Date
renovate[bot] 2b05177713 build(deps): update rust crate log to 0.4.32 2026-06-21 18:12:46 +00:00
renovate[bot] ff2d7d415f build(deps): lock file maintenance lockfile maintenance (#7537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-21 16:54:46 +02:00
renovate[bot] 2f75c0ed34 build(deps): update codecov/codecov-action action to v7 (#7552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-21 16:31:29 +02:00
renovate[bot] a765690f30 build(deps): update rust crate notify-rust to 4.18.0 2026-06-20 20:42:44 +00:00
renovate[bot] cd75be40f3 build(deps): update rust crate which to 8.0.4 2026-06-20 16:52:59 +00:00
renovate[bot] 1111230289 build(deps): update taiki-e/install-action action to v2.81.10 2026-06-15 06:32:03 +00:00
renovate[bot] b78f271fee build(deps): update codecov/codecov-action action to v6.0.2 2026-06-15 01:10:33 +00:00
renovate[bot] 682fa5f312 build(deps): update rust crate regex to 1.12.4 2026-06-13 16:34:44 +00:00
renovate[bot] 671f23140e build(deps): update rust crate which to 8.0.3 2026-06-12 01:52:54 +00:00
renovate[bot] 2ae09e226a build(deps): update taiki-e/install-action action to v2.81.3 (#7542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-08 11:40:27 +00:00
renovate[bot] 91efd25889 build(deps): update crate-ci/typos action to v1.47.2 2026-06-08 04:30:42 +00:00
renovate[bot] 49cf2b016f build(deps): update embarkstudios/cargo-deny-action action to v2.0.20 (#7541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-08 04:29:38 +00:00
renovate[bot] dc60c13b0d build(deps): update dprint plugins to v0.22.1 (#7540)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-08 04:28:37 +00:00
renovate[bot] fea4e5155a build(deps): update actions/checkout action to v6.0.3 2026-06-08 02:11:49 +00:00
renovate[bot] f595899021 build(deps): update rust crate systemstat to v0.2.7 (#7538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 21:11:35 +00:00
renovate[bot] 2b03c4e721 build(deps): update rust crate jiff to 0.2.28 2026-06-06 16:45:27 +00:00
David Knaack 3dd8c14144 feat(time): improve timezone handling by switching to jiff (#7222) 2026-06-06 18:44:15 +02:00
renovate[bot] 033f20b461 build(deps): update unicode crates to 1.13.3 2026-06-05 16:37:33 +00:00
renovate[bot] da264890e3 build(deps): update rust crate toml_edit to 0.25.12 2026-05-31 16:26:35 +00:00
David Knaack 57bb99bd0d fix: improve reliability of config-file writing (#5426) 2026-05-31 17:31:17 +02:00
David Knaack 9a17d3a4e2 chore(serde): support enum deserialization (#3941) 2026-05-31 13:49:58 +02:00
David Knaack f28f7791a9 chore: add ai policy (#7481) 2026-05-31 13:48:15 +02:00
renovate[bot] 4197977efe build(deps): update rust crate gix to 0.84.0 2026-05-30 06:04:15 +00:00
renovate[bot] a496165316 build(deps): update dependency ziglang to v0.16.0 2026-05-29 17:05:50 +00:00
Bruno Verachten 166d7bb30a fix: use cargo-zigbuild for riscv64gc-unknown-linux-musl release builds (#7449) 2026-05-29 19:05:12 +02:00
renovate[bot] 528332ee61 build(deps): update rust crate log to 0.4.30 2026-05-29 11:49:26 +00:00
lif cfd5e7619e feat(pixi): expose PIXI_PROJECT_NAME as format placeholder (#7346)
Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: David Knaack <davidkna@users.noreply.github.com>
2026-05-28 22:40:19 +02:00
Asish Kumar 26ce2ccf4c feat(git_state): show git am progress (#7500) 2026-05-28 22:36:03 +02:00
renovate[bot] 712210ecfe build(deps): update dependency node to v24.16.0 2026-05-26 04:59:53 +00:00
renovate[bot] 3df4a67591 build(deps): update rust crate serde_json to 1.0.150 2026-05-25 22:25:23 +00:00
renovate[bot] dc5c898dd1 build(deps): update taiki-e/install-action action to v2.79.3 (#7503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 08:08:20 +00:00
renovate[bot] eb54ec12c8 build(deps): update dprint plugins (#7502)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 04:34:02 +00:00
renovate[bot] 831cffbb22 build(deps): update crate-ci/typos action to v1.46.2 (#7501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 04:33:13 +00:00
renovate[bot] dacc8e9408 build(deps): update codecov/codecov-action action to v6.0.1 2026-05-25 02:10:37 +00:00
renovate[bot] db39a6539d build(deps): update rust crate os_info to 3.15.0 2026-05-23 21:01:22 +00:00
Nico Mayer 96c1f90eeb fix(nodejs): avoid deno project files (#7478)
* detect deno project files

* deno.jsonl to deno.jsonc

* typo in docs
2026-05-23 15:30:39 +02:00
Swayam Rohit Gavankar 05ee20141e docs: update advanced-config docs with zsh prompt indent instructions (#7447) 2026-05-22 19:51:50 +02:00
renovate[bot] 0d8ca602ad build(deps): update rust crate quick-xml to 0.40.1 2026-05-19 18:27:05 +00:00
renovate[bot] 7499e283b3 build(deps): update embarkstudios/cargo-deny-action action to v2.0.18 (#7486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 18:25:51 +00:00
renovate[bot] 54e861221c build(deps): update taiki-e/install-action action to v2.77.7 (#7487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 05:04:55 +00:00
renovate[bot] 14a5f870af build(deps): update crate-ci/typos action to v1.46.1 2026-05-18 00:40:47 +00:00
renovate[bot] e521f39078 build(deps): update rust crate quick-xml to 0.40.0 (#7477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-17 11:29:15 +02:00
renovate[bot] 17cf5317eb build(deps): update rust crate open to 5.3.5 2026-05-16 00:42:39 +00:00
renovate[bot] 3ebbeae1a7 build(deps): update rust crate clap_complete to 4.6.5 2026-05-15 21:54:42 +00:00
renovate[bot] 5162c4159b build(deps): pin dependencies (#7476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 19:37:22 +02:00
renovate[bot] b76e16c38c build(deps): update rust crate jsonc-parser to 0.32.4 (#7475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 16:55:22 +00:00
renovate[bot] 7d9cbeb5b7 build(deps): update rust crate clap_complete to 4.6.4 (#7474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 16:21:02 +02:00
renovate[bot] 41f2030db1 build(deps): update rust crate quick-xml to 0.39.4 (#7472)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 13:45:06 +00:00
renovate[bot] e1d8dea865 build(deps): update rust crate nix to 0.31.3 2026-05-15 09:45:02 +00:00
renovate[bot] fe0296bc8f build(deps): update taiki-e/install-action action to v2.77.1 (#7470)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 05:57:36 +00:00
renovate[bot] ffe59d6787 build(deps): update dependency dprint/dprint-plugin-typescript to v0.96.0 2026-05-11 00:44:58 +00:00
Madeline b85b7b9c52 feat(nix-shell): Add level variable to show nix shell depth (#7394) 2026-05-10 22:08:00 +02:00
Rayan Salhab d455255e7b fix(maven): avoid detecting user .mvn config as project (#7426)
---------

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-05-10 22:03:42 +02:00
Truffle d0e246802c fix(gcloud): honor CLOUDSDK_COMPUTE_REGION env variable (#7451) 2026-05-10 21:57:38 +02:00
renovate[bot] a54b62bfb1 build(deps): update rust crate quick-xml to 0.39.3 (#7468)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 10:28:30 +02:00
renovate[bot] e5a1f9c33d build(deps): update rust crate notify-rust to 4.17.0 2026-05-09 01:34:05 +00:00
renovate[bot] ed582dd038 build(deps): update actions/setup-node action to v6.4.0 (#7457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 21:49:24 +00:00
renovate[bot] cd0b573326 build(deps): update crate-ci/typos action to v1.46.0 2026-05-04 18:09:26 +00:00
renovate[bot] 10daba00c0 build(deps): update taiki-e/install-action action to v2.75.27 (#7459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 13:57:31 +00:00
renovate[bot] c9bacab812 build(deps): update signpath/github-action-submit-signing-request action to v2.2 (#7458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 09:03:02 +00:00
renovate[bot] 30c7f513d3 build(deps): update rust-lang/crates-io-auth-action action to v1.0.4 (#7456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 05:16:35 +00:00
renovate[bot] bea2843088 build(deps): pin dependency vitepress to 1.6.4 2026-05-04 01:26:15 +00:00
David Knaack 6a2096608e chore: rename master branch to main (#7446) 2026-04-30 21:54:56 +02:00
Matan Kushner 387c2f6e28 docs(i18n): new Crowdin updates (#7404)
New translations readme.md (Spanish)
2026-05-01 04:33:53 +09:00
36 changed files with 1692 additions and 1283 deletions
+2 -2
View File
@@ -25,9 +25,9 @@
"target/" "target/"
], ],
"plugins": [ "plugins": [
"https://github.com/dprint/dprint-plugin-typescript/releases/download/0.95.15/plugin.wasm", "https://github.com/dprint/dprint-plugin-typescript/releases/download/0.96.1/plugin.wasm",
"https://github.com/dprint/dprint-plugin-json/releases/download/0.21.3/plugin.wasm", "https://github.com/dprint/dprint-plugin-json/releases/download/0.21.3/plugin.wasm",
"https://github.com/dprint/dprint-plugin-markdown/releases/download/0.21.1/plugin.wasm", "https://github.com/dprint/dprint-plugin-markdown/releases/download/0.22.1/plugin.wasm",
"https://github.com/dprint/dprint-plugin-toml/releases/download/0.7.0/plugin.wasm" "https://github.com/dprint/dprint-plugin-toml/releases/download/0.7.0/plugin.wasm"
] ]
} }
+15
View File
@@ -16,12 +16,27 @@ Closes #
<!--- Please describe in detail how you tested your changes. --> <!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how --> <!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. --> <!--- your change affects other areas of the code, etc. -->
<!--- Most changes do not need to be tested on multiple operating systems. -->
- [ ] I have tested using **MacOS** - [ ] I have tested using **MacOS**
- [ ] I have tested using **Linux** - [ ] I have tested using **Linux**
- [ ] I have tested using **Windows** - [ ] I have tested using **Windows**
#### AI-Assistance
<!-- Per our AI Policy (AI_POLICY.md), all AI usage must be declared. -->
Have you used AI-assistance to author this PR?
- [ ] Yes
- [ ] No
If **yes**, describe the scope of assistance:
<!--- Describe how you used AI-Assistance -->
<!--- Disclosure is required if you have used AI-assistance -->
<!--- For example: answering project questions, writing tests, implementation, or documentation -->
N/A
#### Checklist: #### Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. --> <!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> <!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I have updated the documentation accordingly. - [ ] I have updated the documentation accordingly.
- [ ] I have updated the tests accordingly. - [ ] I have updated the tests accordingly.
- [ ] I understand and have read the code I contribute and can answer questions about it.
+10 -8
View File
@@ -1076,9 +1076,7 @@
"detect_files": [ "detect_files": [
"pom.xml" "pom.xml"
], ],
"detect_folders": [ "detect_folders": []
".mvn"
]
} }
}, },
"memory_usage": { "memory_usage": {
@@ -1211,7 +1209,10 @@
".nvmrc", ".nvmrc",
"!bunfig.toml", "!bunfig.toml",
"!bun.lock", "!bun.lock",
"!bun.lockb" "!bun.lockb",
"!deno.json",
"!deno.jsonc",
"!deno.lock"
], ],
"detect_folders": [ "detect_folders": [
"node_modules" "node_modules"
@@ -4846,9 +4847,7 @@
"items": { "items": {
"type": "string" "type": "string"
}, },
"default": [ "default": []
".mvn"
]
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -5203,7 +5202,10 @@
".nvmrc", ".nvmrc",
"!bunfig.toml", "!bunfig.toml",
"!bun.lock", "!bun.lock",
"!bun.lockb" "!bun.lockb",
"!deno.json",
"!deno.jsonc",
"!deno.lock"
] ]
}, },
"detect_folders": { "detect_folders": {
+11
View File
@@ -135,5 +135,16 @@
], ],
datasourceTemplate: 'crate', datasourceTemplate: 'crate',
}, },
{
customType: 'regex',
managerFilePatterns: [
'/^(workflow-templates|\\.github\\/workflows)\\/[^/]+\\.ya?ml$/',
],
matchStrings: [
'ziglang==(?<currentValue>\\S+)',
],
datasourceTemplate: 'pypi',
depNameTemplate: 'ziglang',
},
], ],
} }
+7 -7
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Docs | Format - name: Docs | Format
uses: dprint/check@9cb3a2b17a8e606d37aae341e49df3654933fc23 # v2.3 uses: dprint/check@9cb3a2b17a8e606d37aae341e49df3654933fc23 # v2.3
@@ -22,9 +22,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install | Taplo - name: Install | Taplo
uses: taiki-e/install-action@7a4939c09608b2a1986b484eca1d16fd0db8ebef # v2.75.5 uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
with: with:
tool: taplo-cli@0.10.0 tool: taplo-cli@0.10.0
- name: Presets | Validate with schema - name: Presets | Validate with schema
@@ -37,7 +37,7 @@ jobs:
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref != 'i18n_master' }} if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref != 'i18n_master' }}
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Prevent File Change - name: Prevent File Change
@@ -64,12 +64,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Node - name: Setup | Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24.15.0 node-version: 24.16.0
cache: 'npm' cache: 'npm'
cache-dependency-path: docs/package-lock.json cache-dependency-path: docs/package-lock.json
+3 -3
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Lint | Run shellcheck - name: Lint | Run shellcheck
run: shellcheck --severity=warning install/**/*.sh run: shellcheck --severity=warning install/**/*.sh
@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Install shfmt - name: Setup | Install shfmt
run: | run: |
@@ -42,7 +42,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Test | Piped execution with curl - name: Test | Piped execution with curl
run: | run: |
+3 -3
View File
@@ -7,12 +7,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Node - name: Setup | Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24.15.0 node-version: 24.16.0
cache: 'npm' cache: 'npm'
cache-dependency-path: docs/package-lock.json cache-dependency-path: docs/package-lock.json
+30 -15
View File
@@ -2,7 +2,7 @@ name: Release
on: on:
push: push:
branches: branches:
- master - main
env: env:
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10 CARGO_NET_RETRY: 10
@@ -58,6 +58,7 @@ jobs:
- target: riscv64gc-unknown-linux-musl - target: riscv64gc-unknown-linux-musl
os: ubuntu-latest os: ubuntu-latest
name: starship-riscv64gc-unknown-linux-musl.tar.gz name: starship-riscv64gc-unknown-linux-musl.tar.gz
use_zigbuild: true
- target: x86_64-apple-darwin - target: x86_64-apple-darwin
os: macos-latest os: macos-latest
@@ -92,7 +93,7 @@ jobs:
RUSTFLAGS: ${{ matrix.rustflags || '' }} RUSTFLAGS: ${{ matrix.rustflags || '' }}
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Rust - name: Setup | Rust
run: rustup toolchain install stable --profile minimal --target ${{ matrix.target }} --no-self-update run: rustup toolchain install stable --profile minimal --target ${{ matrix.target }} --no-self-update
@@ -106,19 +107,33 @@ jobs:
RUSTFLAGS: "" RUSTFLAGS: ""
- name: Setup | Install cross [Linux] - name: Setup | Install cross [Linux]
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest' && !matrix.use_zigbuild
uses: taiki-e/install-action@7a4939c09608b2a1986b484eca1d16fd0db8ebef # v2.75.5 uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
with: with:
tool: cross@0.2.5 tool: cross@0.2.5
- name: Setup | Install Zig [riscv64]
if: matrix.use_zigbuild
run: pip install ziglang==0.16.0
- name: Setup | Install cargo-zigbuild [riscv64]
if: matrix.use_zigbuild
uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
with:
tool: cargo-zigbuild
- name: Build | Build [Cargo] - name: Build | Build [Cargo]
if: matrix.os != 'ubuntu-latest' if: matrix.os != 'ubuntu-latest'
run: cargo build --release --locked --target ${{ matrix.target }} run: cargo build --release --locked --target ${{ matrix.target }}
- name: Build | Build [Cross] - name: Build | Build [Cross]
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest' && !matrix.use_zigbuild
run: cross build --release --locked --target ${{ matrix.target }} run: cross build --release --locked --target ${{ matrix.target }}
- name: Build | Build [Zigbuild]
if: matrix.use_zigbuild
run: cargo zigbuild --release --locked --target ${{ matrix.target }}
- name: Build | Installer [Windows] - name: Build | Installer [Windows]
continue-on-error: true continue-on-error: true
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
@@ -141,7 +156,7 @@ jobs:
- name: Sign | Sign [Windows] - name: Sign | Sign [Windows]
continue-on-error: true continue-on-error: true
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
uses: signpath/github-action-submit-signing-request@bc66d86b015a46e9c6d9700de73143a82f9570ff # v2.1 uses: signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2.2
with: with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '${{ vars.SIGNPATH_ORGANIZATION_ID }}' organization-id: '${{ vars.SIGNPATH_ORGANIZATION_ID }}'
@@ -204,10 +219,10 @@ jobs:
STARSHIP_VERSION: ${{ needs.release_please.outputs.tag_name }} STARSHIP_VERSION: ${{ needs.release_please.outputs.tag_name }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
# Required to include the recently merged Crowdin PR # Required to include the recently merged Crowdin PR
ref: master ref: main
- name: Notarize | Set up secrets - name: Notarize | Set up secrets
env: env:
@@ -241,9 +256,9 @@ jobs:
xcrun notarytool store-credentials "$KEYCHAIN_ENTRY" --team-id "$APPLEID_TEAMID" --apple-id "$APPLEID_USERNAME" --password "$APPLEID_PASSWORD" --keychain "$KEYCHAIN_PATH" xcrun notarytool store-credentials "$KEYCHAIN_ENTRY" --team-id "$APPLEID_TEAMID" --apple-id "$APPLEID_USERNAME" --password "$APPLEID_PASSWORD" --keychain "$KEYCHAIN_PATH"
- name: Setup | Node - name: Setup | Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 24.15.0 node-version: 24.16.0
- name: Notarize | Build docs - name: Notarize | Build docs
run: | run: |
@@ -317,13 +332,13 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Rust - name: Setup | Rust
run: rustup toolchain install stable --profile minimal --no-self-update run: rustup toolchain install stable --profile minimal --no-self-update
- name: Build | Authenticate - name: Build | Authenticate
uses: rust-lang/crates-io-auth-action@b7e9a28eded4986ec6b1fa40eeee8f8f165559ec # v1.0.3 uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4
id: auth id: auth
- name: Build | Publish - name: Build | Publish
@@ -375,7 +390,7 @@ jobs:
if: ${{ needs.release_please.outputs.release_created == 'true' }} if: ${{ needs.release_please.outputs.release_created == 'true' }}
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Artifacts - name: Setup | Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- run: pwsh ./install/windows/choco/update.ps1 - run: pwsh ./install/windows/choco/update.ps1
@@ -391,7 +406,7 @@ jobs:
continue-on-error: true continue-on-error: true
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Merge | Merge Crowdin PR - name: Merge | Merge Crowdin PR
run: gh pr merge i18n_master --squash --repo=starship/starship run: gh pr merge i18n_master --squash --repo=starship/starship
env: env:
@@ -403,7 +418,7 @@ jobs:
needs: merge_crowdin_pr needs: merge_crowdin_pr
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Trigger workflow dispatch - name: Trigger workflow dispatch
run: gh workflow run publish-docs.yml run: gh workflow run publish-docs.yml
env: env:
+2 -2
View File
@@ -22,8 +22,8 @@ jobs:
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Test | Security Audit - name: Test | Security Audit
uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb # v2.0.17 uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20
with: with:
command: check ${{ matrix.checks }} command: check ${{ matrix.checks }}
+2 -2
View File
@@ -6,5 +6,5 @@ jobs:
name: Spell Check with Typos name: Spell Check with Typos
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: crate-ci/typos@7c572958218557a3272c2d6719629443b5cc26fd # v1.45.2 - uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # v1.47.2
+10 -10
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Rust - name: Setup | Rust
run: rustup toolchain install stable --profile minimal --component rustfmt --no-self-update run: rustup toolchain install stable --profile minimal --component rustfmt --no-self-update
@@ -41,7 +41,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Rust - name: Setup | Rust
run: rustup toolchain install stable --profile minimal --component clippy --no-self-update run: rustup toolchain install stable --profile minimal --component clippy --no-self-update
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Rust - name: Setup | Rust
run: rustup toolchain install stable --profile minimal --no-self-update run: rustup toolchain install stable --profile minimal --no-self-update
@@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Rust - name: Setup | Rust
run: rustup toolchain install stable --profile minimal --no-self-update run: rustup toolchain install stable --profile minimal --no-self-update
@@ -94,7 +94,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Rust - name: Setup | Rust
run: rustup toolchain install stable --profile minimal --no-self-update run: rustup toolchain install stable --profile minimal --no-self-update
@@ -114,7 +114,7 @@ jobs:
contents: read contents: read
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup | Rust - name: Setup | Rust
run: rustup toolchain install stable --profile minimal --no-self-update run: rustup toolchain install stable --profile minimal --no-self-update
@@ -150,7 +150,7 @@ jobs:
RUSTFLAGS: ${{ matrix.rustflags || '' }} RUSTFLAGS: ${{ matrix.rustflags || '' }}
steps: steps:
- name: Setup | Checkout - name: Setup | Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# Install all the required dependencies for testing # Install all the required dependencies for testing
- name: Setup | Rust - name: Setup | Rust
@@ -162,7 +162,7 @@ jobs:
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Install cargo-llvm-cov - name: Install cargo-llvm-cov
uses: taiki-e/install-action@7a4939c09608b2a1986b484eca1d16fd0db8ebef # v2.75.5 uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
with: with:
tool: cargo-llvm-cov@0.8.5 tool: cargo-llvm-cov@0.8.5
@@ -234,7 +234,7 @@ jobs:
target/debug/starship-x86_64-pc-windows-msvc.msi target/debug/starship-x86_64-pc-windows-msvc.msi
- name: Sign | Sign [Windows] - name: Sign | Sign [Windows]
uses: signpath/github-action-submit-signing-request@bc66d86b015a46e9c6d9700de73143a82f9570ff # v2.1 uses: signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2.2
continue-on-error: true continue-on-error: true
if: matrix.os == 'windows-latest' && matrix.rust == 'stable' && github.event_name == 'push' && github.repository == 'starship/starship' if: matrix.os == 'windows-latest' && matrix.rust == 'stable' && github.event_name == 'push' && github.repository == 'starship/starship'
with: with:
@@ -247,7 +247,7 @@ jobs:
output-artifact-directory: target/debug output-artifact-directory: target/debug
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
if: github.repository == 'starship/starship' if: github.repository == 'starship/starship'
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
+41
View File
@@ -0,0 +1,41 @@
# AI Usage Policy
This policy supplements our [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md).
This project is maintained by volunteers.
This policy exists to keep review burden reasonable.
The policy is inspired by the [Ghostty](https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md) and [LLVM](https://llvm.org/docs/AIToolPolicy.html) AI policies.
Contributions that violate this policy may be closed without further notice.
## Mandatory Disclosure
Every Pull Request that utilizes AI-assisted tooling (including but not limited to Claude Code, Cursor, GitHub Copilot, ChatGPT, or local LLMs) must disclose its usage.
### PR Description
You must complete the **AI-Assistance** section in our Pull Request Template.
## Human-in-the-Loop
Contributors must fully understand all submitted contributions.
### Contributions
- You must be able to explain what your changes do and defend your implementation choices.
- You are expected to have read and understood every line of code you submit.
- If your response to a maintainer's question is an unedited copy-paste from an LLM, or if you cannot explain the mechanics of your PR, the PR will be closed.
### Issue Triage and Discussions
You are not allowed to reply to user issues or discussions with unverified or raw AI-generated information.
## "Good First Issue" Protections
You may not submit contributions to close a `🌱 good first issue` if they were authored with substantial AI assistance.
These issues are intentionally triaged as learning opportunities for new developers navigating the codebase for the first time.
## Low-Effort Contributions & Prohibition of Autonomous Agents
- Contributions that are overly verbose, contain unsupported or hallucinated claims, or otherwise show the hallmarks of low-effort LLM usage may be closed without further notice.
- Contributions via OpenClaw, or any other unsupervised autonomous agent operating in an automated loop, are strictly prohibited.
+2 -2
View File
@@ -273,10 +273,10 @@ Once setup is complete, you can refer to VitePress documentation on the actual i
This is our preferred process for opening a PR on GitHub: This is our preferred process for opening a PR on GitHub:
1. Fork this repository 1. Fork this repository
2. Create a branch off of `master` for your work: `git checkout -b my-feature-branch` 2. Create a branch off of `main` for your work: `git checkout -b my-feature-branch`
3. Make some changes, committing them along the way 3. Make some changes, committing them along the way
4. When your changes are ready for review, push your branch: `git push origin my-feature-branch` 4. When your changes are ready for review, push your branch: `git push origin my-feature-branch`
5. Create a pull request from your branch to `starship/master` 5. Create a pull request from your branch to `starship/main`
6. No need to assign the pull request to anyone, we'll review it when we can 6. No need to assign the pull request to anyone, we'll review it when we can
7. When the changes have been reviewed and approved, someone will squash and merge for you 7. When the changes have been reviewed and approved, someone will squash and merge for you
Generated
+378 -639
View File
File diff suppressed because it is too large Load Diff
+18 -18
View File
@@ -35,53 +35,54 @@ config-schema = ["schemars"]
notify = ["notify-rust"] notify = ["notify-rust"]
[dependencies] [dependencies]
chrono = { version = "0.4.44", default-features = false, features = ["clock", "std", "wasmbind"] }
clap = { version = "4.6.1", features = ["derive", "cargo", "unicode"] } clap = { version = "4.6.1", features = ["derive", "cargo", "unicode"] }
clap_complete = "4.6.3" clap_complete = "4.6.5"
clap_complete_nushell = "4.6.0" clap_complete_nushell = "4.6.0"
dirs = "6.0.0" dirs = "6.0.0"
dunce = "1.0.5" dunce = "1.0.5"
# default feature restriction addresses https://github.com/starship/starship/issues/4251 # default feature restriction addresses https://github.com/starship/starship/issues/4251
gix = { version = "0.83.0", default-features = false, features = ["max-performance-safe", "revision", "zlib-rs", "status", "sha1"] } gix = { version = "0.84.0", default-features = false, features = ["max-performance-safe", "revision", "zlib-rs", "status", "sha1"] }
indexmap = { version = "2.14.0", features = ["serde"] } indexmap = { version = "2.14.0", features = ["serde"] }
jsonc-parser = { version = "0.32.3", features = ["serde"] } jiff = { version = "0.2.28", features = ["serde"] }
log = { version = "0.4.29", features = ["std"] } jsonc-parser = { version = "0.32.4", features = ["serde"] }
log = { version = "0.4.32", features = ["std"] }
# notify-rust is optional (on by default) because the crate doesn't currently build for darwin with nix # notify-rust is optional (on by default) because the crate doesn't currently build for darwin with nix
# see: https://github.com/NixOS/nixpkgs/issues/160876 # see: https://github.com/NixOS/nixpkgs/issues/160876
notify-rust = { version = "4.16.1", optional = true } notify-rust = { version = "4.18.0", optional = true }
nu-ansi-term = "0.50.3" nu-ansi-term = "0.50.3"
open = "5.3.4" open = "5.3.5"
# update os module config and tests when upgrading os_info # update os module config and tests when upgrading os_info
os_info = { version = "3.14.0", features = ["schemars"] } os_info = { version = "3.15.0", features = ["schemars"] }
# for efficient shared state between `git_status` and `git_metrics`, allowing parallel printing. This is for poison-free locks. # for efficient shared state between `git_status` and `git_metrics`, allowing parallel printing. This is for poison-free locks.
parking_lot = "0.12.5" parking_lot = "0.12.5"
path-slash = "0.2.1" path-slash = "0.2.1"
pest = "2.8.6" pest = "2.8.6"
pest_derive = "2.8.6" pest_derive = "2.8.6"
process_control = "5.2.0" process_control = "5.2.0"
quick-xml = "0.39.2" quick-xml = "0.40.1"
rand = "0.10.1" rand = "0.10.1"
rayon = "1.12.0" rayon = "1.12.0"
regex = { version = "1.12.3", default-features = false, features = ["perf", "std", "unicode-perl"] } regex = { version = "1.12.4", default-features = false, features = ["perf", "std", "unicode-perl"] }
rust-ini = "0.21.3" rust-ini = "0.21.3"
semver = "1.0.28" semver = "1.0.28"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.150"
sha1 = "0.11.0" sha1 = "0.11.0"
shadow-rs = { version = "2.0.0", default-features = false, features = ["build"] } shadow-rs = { version = "2.0.0", default-features = false, features = ["build"] }
# battery is optional (on by default) because the crate doesn't currently build for Termux # battery is optional (on by default) because the crate doesn't currently build for Termux
# see: https://github.com/svartalf/rust-battery/issues/33 # see: https://github.com/svartalf/rust-battery/issues/33
starship-battery = { version = "0.11.0", optional = true } starship-battery = { version = "0.11.0", optional = true }
strsim = "0.11.1" strsim = "0.11.1"
systemstat = "=0.2.6" systemstat = "=0.2.7"
tempfile = "3.27.0"
terminal_size = "0.4.4" terminal_size = "0.4.4"
toml = { version = "1.1.2", features = ["preserve_order"] } toml = { version = "1.1.2", features = ["preserve_order"] }
toml_edit = "0.25.11" toml_edit = "0.25.12"
unicode-segmentation = "1.13.2" unicode-segmentation = "1.13.3"
unicode-width = "0.2.2" unicode-width = "0.2.2"
urlencoding = "2.1.3" urlencoding = "2.1.3"
versions = "7.0.0" versions = "7.0.0"
which = "8.0.2" which = "8.0.4"
whoami = { version = "2.1.2", default-features = false, features = ["std", "wasi-wasite"] } whoami = { version = "2.1.2", default-features = false, features = ["std", "wasi-wasite"] }
yaml-rust2 = "0.11.0" yaml-rust2 = "0.11.0"
@@ -108,7 +109,7 @@ features = [
] ]
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]
nix = { version = "0.31.2", default-features = false, features = ["feature", "fs", "user"] } nix = { version = "0.31.3", default-features = false, features = ["feature", "fs", "user"] }
[build-dependencies] [build-dependencies]
shadow-rs = { version = "2.0.0", default-features = false, features = ["build"] } shadow-rs = { version = "2.0.0", default-features = false, features = ["build"] }
@@ -118,8 +119,7 @@ dunce = "1.0.5"
winres = "0.1.12" winres = "0.1.12"
[dev-dependencies] [dev-dependencies]
mockall = "0.14.0" mockall = "=0.14.0"
tempfile = "3.27.0"
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1
+43 -32
View File
@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<img <img
width="400" width="400"
src="https://raw.githubusercontent.com/starship/starship/master/media/logo.png" src="https://raw.githubusercontent.com/starship/starship/main/media/logo.png"
alt="Starship Cross-shell prompt" alt="Starship Cross-shell prompt"
/> />
</p> </p>
@@ -9,7 +9,7 @@
<p align="center"> <p align="center">
<a href="https://github.com/starship/starship/actions" <a href="https://github.com/starship/starship/actions"
><img ><img
src="https://img.shields.io/github/actions/workflow/status/starship/starship/workflow.yml?branch=master&label=workflow&style=flat-square" src="https://img.shields.io/github/actions/workflow/status/starship/starship/workflow.yml?branch=main&label=workflow&style=flat-square"
alt="GitHub Actions workflow status" alt="GitHub Actions workflow status"
/></a> /></a>
<a href="https://crates.io/crates/starship" <a href="https://crates.io/crates/starship"
@@ -48,114 +48,125 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/starship/starship/blob/master/README.md" <a href="https://github.com/starship/starship/blob/main/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-us.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-us.png"
alt="English" alt="English"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/de-DE/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/de-DE/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-de.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-de.png"
alt="Deutsch" alt="Deutsch"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/es-ES/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/es-ES/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-es.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-es.png"
alt="Español" alt="Español"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/fr-FR/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/fr-FR/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-fr.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-fr.png"
alt="Français" alt="Français"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/id-ID/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/id-ID/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-id.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-id.png"
alt="Bahasa Indonesia" alt="Bahasa Indonesia"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/it-IT/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/it-IT/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-it.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-it.png"
alt="Italiano" alt="Italiano"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/ja-JP/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/ja-JP/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-jp.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-jp.png"
alt="日本語" alt="日本語"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/pt-BR/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/pt-BR/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-br.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-br.png"
alt="Português do Brasil" alt="Português do Brasil"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/ru-RU/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/ru-RU/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-ru.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-ru.png"
alt="Русский" alt="Русский"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/uk-UA/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/uk-UA/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-ua.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-ua.png"
alt="Українська" alt="Українська"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/vi-VN/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/vi-VN/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-vn.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-vn.png"
alt="Tiếng Việt" alt="Tiếng Việt"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/zh-CN/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/zh-CN/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-cn.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-cn.png"
alt="简体中文" alt="简体中文"
/></a> /></a>
&nbsp; &nbsp;
<a <a
href="https://github.com/starship/starship/blob/master/docs/zh-TW/guide/README.md" href="https://github.com/starship/starship/blob/main/docs/zh-TW/guide/README.md"
><img ><img
height="20" height="20"
src="https://raw.githubusercontent.com/starship/starship/master/media/flag-tw.png" src="https://raw.githubusercontent.com/starship/starship/main/media/flag-tw.png"
alt="繁體中文" alt="繁體中文"
/></a> /></a>
</p> </p>
<h1></h1> <h1></h1>
> [!WARNING]
> **The default branch has been renamed from `master` to `main`.**
> If you have a local clone, update it by running:
>
> ```sh
> git branch -m master main
> git fetch origin
> git branch -u origin/main main
> git remote set-head origin -a
> ```
<img <img
src="https://raw.githubusercontent.com/starship/starship/master/media/demo.gif" src="https://raw.githubusercontent.com/starship/starship/main/media/demo.gif"
alt="Starship with iTerm2 and the Snazzy theme" alt="Starship with iTerm2 and the Snazzy theme"
width="50%" width="50%"
align="right" align="right"
@@ -414,7 +425,7 @@ We are always looking for contributors of **all skill levels**! If you're lookin
If you are fluent in a non-English language, we greatly appreciate any help keeping our docs translated and up-to-date in other languages. If you would like to help, translations can be contributed on the [Starship Crowdin](https://translate.starship.rs/). If you are fluent in a non-English language, we greatly appreciate any help keeping our docs translated and up-to-date in other languages. If you would like to help, translations can be contributed on the [Starship Crowdin](https://translate.starship.rs/).
If you are interested in helping contribute to starship, please take a look at our [Contributing Guide](https://github.com/starship/starship/blob/master/CONTRIBUTING.md). Also, feel free to drop into our [Discord server](https://discord.gg/8Jzqu3T) and say hi. 👋 If you are interested in helping contribute to starship, please take a look at our [Contributing Guide](https://github.com/starship/starship/blob/main/CONTRIBUTING.md). Also, feel free to drop into our [Discord server](https://discord.gg/8Jzqu3T) and say hi. 👋
## 💭 Inspired By ## 💭 Inspired By
@@ -443,13 +454,13 @@ This program will not transfer any information to other networked systems unless
<p align="center"> <p align="center">
<br> <br>
<img width="100" src="https://raw.githubusercontent.com/starship/starship/master/media/icon.png" alt="Starship rocket icon"> <img width="100" src="https://raw.githubusercontent.com/starship/starship/main/media/icon.png" alt="Starship rocket icon">
</p> </p>
## 📝 License ## 📝 License
Copyright © 2019-present, [Starship Contributors](https://github.com/starship/starship/graphs/contributors).<br> Copyright © 2019-present, [Starship Contributors](https://github.com/starship/starship/graphs/contributors).<br>
This project is [ISC](https://github.com/starship/starship/blob/master/LICENSE) licensed. This project is [ISC](https://github.com/starship/starship/blob/main/LICENSE) licensed.
[alpine linux packages]: https://pkgs.alpinelinux.org/packages?name=starship [alpine linux packages]: https://pkgs.alpinelinux.org/packages?name=starship
[arch linux extra]: https://archlinux.org/packages/extra/x86_64/starship [arch linux extra]: https://archlinux.org/packages/extra/x86_64/starship
+2 -2
View File
@@ -37,7 +37,7 @@ fn gen_presets_hook(mut file: &File) -> SdResult<()> {
.and_then(|v| v.strip_suffix(".toml")) .and_then(|v| v.strip_suffix(".toml"))
.expect("Failed to process filename"); .expect("Failed to process filename");
presets.push_str(format!("print::Preset(\"{name}\"),\n").as_str()); presets.push_str(format!("print::Preset(\"{name}\"),\n").as_str());
match_arms.push_str(format!(r#""{name}" => include_bytes!(r"{full_path}"),"#).as_str()); match_arms.push_str(format!(r#""{name}" => include_str!(r"{full_path}"),"#).as_str());
} }
writeln!( writeln!(
@@ -51,7 +51,7 @@ pub fn get_preset_list<'a>() -> &'a [print::Preset] {{
] ]
}} }}
pub fn get_preset_content(name: &str) -> &[u8] {{ pub fn get_preset_content(name: &str) -> &str {{
match name {{ match name {{
{match_arms} {match_arms}
_ => unreachable!(), _ => unreachable!(),
+6
View File
@@ -315,6 +315,12 @@ Produces a prompt like the following:
▶ starship on  rprompt [!] is 📦 v0.57.0 via 🦀 v1.54.0 took 17s ▶ starship on  rprompt [!] is 📦 v0.57.0 via 🦀 v1.54.0 took 17s
``` ```
When using `zsh` (v5.0.5+), the shell adds a default trailing space to the right prompt. This can cause alignment issues specifically when using the Starship `$fill` module. To remove this gap, add the following to your `.zshrc`:
```zsh
ZLE_RPROMPT_INDENT=0
```
## Continuation Prompt ## Continuation Prompt
Some shells support a continuation prompt along with the normal prompt. This prompt is rendered instead of the normal prompt when the user has entered an incomplete statement (such as a single left parenthesis or quote). Some shells support a continuation prompt along with the normal prompt. This prompt is rendered instead of the normal prompt when the user has entered an incomplete statement (such as a single left parenthesis or quote).
+41 -24
View File
@@ -1824,6 +1824,7 @@ format = '[+$added]($added_style)/[-$deleted]($deleted_style) '
The `gcloud` module shows the current configuration for [`gcloud`](https://cloud.google.com/sdk/gcloud) CLI. The `gcloud` module shows the current configuration for [`gcloud`](https://cloud.google.com/sdk/gcloud) CLI.
This is based on the `~/.config/gcloud/active_config` file and the `~/.config/gcloud/configurations/config_{CONFIG NAME}` file and the `CLOUDSDK_CONFIG` env var. This is based on the `~/.config/gcloud/active_config` file and the `~/.config/gcloud/configurations/config_{CONFIG NAME}` file and the `CLOUDSDK_CONFIG` env var.
The `CLOUDSDK_CORE_PROJECT` and `CLOUDSDK_COMPUTE_REGION` environment variables, when set, override the `project` and `region` values from the active configuration, mirroring the behavior of `gcloud` itself.
When the module is enabled it will always be active, unless `detect_env_vars` has When the module is enabled it will always be active, unless `detect_env_vars` has
been set in which case the module will only be active when one of the been set in which case the module will only be active when one of the
@@ -3343,12 +3344,13 @@ The module will be shown when inside a nix-shell environment.
### Variables ### Variables
| Variable | Example | Description | | Variable | Example | Description |
| -------- | ------- | ------------------------------------ | | -------- | ------- | ----------------------------------------------------------------------------- |
| state | `pure` | The state of the nix-shell | | state | `pure` | The state of the nix-shell |
| name | `lorri` | The name of the nix-shell | | name | `lorri` | The name of the nix-shell |
| symbol | | Mirrors the value of option `symbol` | | level | `1` | The depth level of the nix-shell (Only when using [Lix](https://lix.systems)) |
| style\* | | Mirrors the value of option `style` | | symbol | | Mirrors the value of option `symbol` |
| style\* | | Mirrors the value of option `style` |
*: This variable can only be used as a part of a style string *: This variable can only be used as a part of a style string
@@ -3377,7 +3379,7 @@ By default the module will be shown if any of the following conditions are met:
- The current directory contains a file with the `.js`, `.mjs` or `.cjs` extension - The current directory contains a file with the `.js`, `.mjs` or `.cjs` extension
- The current directory contains a file with the `.ts`, `.mts` or `.cts` extension - The current directory contains a file with the `.ts`, `.mts` or `.cts` extension
Additionally, the module will be hidden by default if the directory contains a `bunfig.toml`, `bun.lock`, or `bun.lockb` file, overriding the above conditions. Additionally, the module will be hidden by default if the directory contains a `deno.json`, `deno.jsonc`, `deno.lock`, `bunfig.toml`, `bun.lock`, or `bun.lockb` file, overriding the above conditions.
### Options ### Options
@@ -3856,7 +3858,8 @@ The `pijul_channel` module shows the active channel of the repo in your current
## Pixi ## Pixi
The `pixi` module shows the installed [pixi](https://pixi.sh) version as well as the activated environment, if `$PIXI_ENVIRONMENT_NAME` is set. The `pixi` module shows the installed [pixi](https://pixi.sh) version as well as the activated
environment and project name, if `$PIXI_ENVIRONMENT_NAME` is set.
> [!TIP] > [!TIP]
> This does not suppress pixi's own prompt modifier, you may want to run `pixi config set shell.change-ps1 false`. > This does not suppress pixi's own prompt modifier, you may want to run `pixi config set shell.change-ps1 false`.
@@ -3878,12 +3881,13 @@ The `pixi` module shows the installed [pixi](https://pixi.sh) version as well as
### Variables ### Variables
| Variable | Example | Description | | Variable | Example | Description |
| ----------- | --------- | ------------------------------------ | | ------------ | ------------ | ------------------------------------ |
| version | `v0.33.0` | The version of `pixi` | | version | `v0.33.0` | The version of `pixi` |
| environment | `py311` | The current pixi environment | | environment | `py311` | The current pixi environment |
| symbol | | Mirrors the value of option `symbol` | | project_name | `my-project` | The current pixi project name |
| style | | Mirrors the value of option `style` | | symbol | | Mirrors the value of option `symbol` |
| style | | Mirrors the value of option `style` |
### Example ### Example
@@ -4810,7 +4814,7 @@ format = 'via [$symbol$workspace]($style) '
## Time ## Time
The `time` module shows the current **local** time. The `time` module shows the current **local** time.
The `format` configuration value is used by the [`chrono`](https://crates.io/crates/chrono) crate to control how the time is displayed. Take a look [at the chrono strftime docs](https://docs.rs/chrono/0.4.7/chrono/format/strftime/index.html) to see what options are available. The `format` configuration value is used by the [`jiff`](https://crates.io/crates/jiff) crate to control how the time is displayed. Take a look [at the jiff strftime docs](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html) to see what options are available.
> [!TIP] > [!TIP]
> This module is disabled by default. > This module is disabled by default.
@@ -4818,15 +4822,15 @@ The `format` configuration value is used by the [`chrono`](https://crates.io/cra
### Options ### Options
| Option | Default | Description | | Option | Default | Description |
| ----------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | | ----------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `format` | `'at [$time]($style) '` | The format string for the module. | | `format` | `'at [$time]($style) '` | The format string for the module. |
| `use_12hr` | `false` | Enables 12 hour formatting | | `use_12hr` | `false` | Enables 12 hour formatting |
| `time_format` | see below | The [chrono format string](https://docs.rs/chrono/0.4.7/chrono/format/strftime/index.html) used to format the time. | | `time_format` | see below | The [jiff format string](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html) used to format the time. |
| `style` | `'bold yellow'` | The style for the module time | | `style` | `'bold yellow'` | The style for the module time |
| `utc_time_offset` | `'local'` | Sets the UTC offset to use. Range from -24 &lt; x &lt; 24. Allows floats to accommodate 30/45 minute timezone offsets. | | `utc_time_offset` | `'local'` | Sets the UTC offset to use. Either an IANA time zone name or a range from -24 &lt; x &lt; 24. Allows floats to accommodate 30/45 minute timezone offsets. |
| `disabled` | `true` | Disables the `time` module. | | `disabled` | `true` | Disables the `time` module. |
| `time_range` | `'-'` | Sets the time range during which the module will be shown. Times must be specified in 24-hours format | | `time_range` | `'-'` | Sets the time range during which the module will be shown. Times must be specified in 24-hours format |
If `use_12hr` is `true`, then `time_format` defaults to `'%r'`. Otherwise, it defaults to `'%T'`. If `use_12hr` is `true`, then `time_format` defaults to `'%r'`. Otherwise, it defaults to `'%T'`.
Manually setting `time_format` will override the `use_12hr` setting. Manually setting `time_format` will override the `use_12hr` setting.
@@ -4842,6 +4846,8 @@ Manually setting `time_format` will override the `use_12hr` setting.
### Example ### Example
#### With UTC offset
```toml ```toml
# ~/.config/starship.toml # ~/.config/starship.toml
@@ -4853,6 +4859,17 @@ utc_time_offset = '-5'
time_range = '10:00:00-14:00:00' time_range = '10:00:00-14:00:00'
``` ```
#### With Timezone name
```toml
# ~/.config/starship.toml
[time]
disabled = false
time_format = '%T'
utc_time_offset = 'Europe/Berlin'
```
## Typst ## Typst
The `typst` module shows the current installed version of Typst used in a project. The `typst` module shows the current installed version of Typst used in a project.
+300 -300
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -5,6 +5,6 @@
"preview": "vitepress preview" "preview": "vitepress preview"
}, },
"devDependencies": { "devDependencies": {
"vitepress": "^1.6.4" "vitepress": "1.6.4"
} }
} }
+1 -1
View File
@@ -30,7 +30,7 @@ impl Default for MavenConfig<'_> {
recursive: false, recursive: false,
detect_extensions: vec![], detect_extensions: vec![],
detect_files: vec!["pom.xml"], detect_files: vec!["pom.xml"],
detect_folders: vec![".mvn"], detect_folders: vec![],
} }
} }
} }
+3
View File
@@ -36,6 +36,9 @@ impl Default for NodejsConfig<'_> {
"!bunfig.toml", "!bunfig.toml",
"!bun.lock", "!bun.lock",
"!bun.lockb", "!bun.lockb",
"!deno.json",
"!deno.jsonc",
"!deno.lock",
], ],
detect_folders: vec!["node_modules"], detect_folders: vec!["node_modules"],
} }
+11 -2
View File
@@ -1,5 +1,13 @@
use crate::config::Either;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// Wrapper struct to enable serde serialization/deserialization for jiff::tz::TimeZone
#[derive(Clone, Deserialize, Serialize)]
#[serde(transparent)]
pub struct TimezoneWrapper(
#[serde(with = "jiff::fmt::serde::tz::required")] pub jiff::tz::TimeZone,
);
#[derive(Clone, Deserialize, Serialize)] #[derive(Clone, Deserialize, Serialize)]
#[cfg_attr( #[cfg_attr(
feature = "config-schema", feature = "config-schema",
@@ -14,7 +22,8 @@ pub struct TimeConfig<'a> {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub time_format: Option<&'a str>, pub time_format: Option<&'a str>,
pub disabled: bool, pub disabled: bool,
pub utc_time_offset: &'a str, #[cfg_attr(feature = "config-schema", schemars(with = "String"))]
pub utc_time_offset: Either<TimezoneWrapper, &'a str>,
pub time_range: &'a str, pub time_range: &'a str,
} }
@@ -26,7 +35,7 @@ impl Default for TimeConfig<'_> {
use_12hr: false, use_12hr: false,
time_format: None, time_format: None,
disabled: true, disabled: true,
utc_time_offset: "local", utc_time_offset: Either::Second("local"),
time_range: "-", time_range: "-",
} }
} }
+13 -9
View File
@@ -8,8 +8,7 @@ use crate::config::StarshipConfig;
use crate::configs::PROMPT_ORDER; use crate::configs::PROMPT_ORDER;
use crate::context::Context; use crate::context::Context;
use crate::utils; use crate::utils;
use std::fs::File; use std::path::PathBuf;
use std::io::Write;
use toml_edit::DocumentMut; use toml_edit::DocumentMut;
#[cfg(not(windows))] #[cfg(not(windows))]
@@ -240,16 +239,17 @@ pub fn get_configuration_edit(context: &Context) -> DocumentMut {
} }
pub fn write_configuration(context: &Context, doc: &DocumentMut) { pub fn write_configuration(context: &Context, doc: &DocumentMut) {
let config_path = context.get_config_path_os().unwrap_or_else(|| { let Some(config_path) = context.get_config_path_os() else {
eprintln!("config path required to write configuration"); eprintln!("config path required to write configuration");
process::exit(1); process::exit(1);
}); };
let config_str = doc.to_string(); let config_path = PathBuf::from(config_path);
File::create(config_path) if let Err(e) = crate::utils::write_file_atomic(config_path, doc.to_string(), true) {
.and_then(|mut file| file.write_all(config_str.as_ref())) eprintln!("Unable to write configuration: {e}");
.expect("Error writing starship config"); process::exit(1);
}
} }
pub fn edit_configuration( pub fn edit_configuration(
@@ -313,7 +313,11 @@ fn get_editor_internal(visual: Option<String>, editor: Option<String>) -> String
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{fs::create_dir, io, path::PathBuf}; use std::{
fs::{File, create_dir},
io::{self, Write},
path::PathBuf,
};
use tempfile::TempDir; use tempfile::TempDir;
use toml_edit::Item; use toml_edit::Item;
+9 -1
View File
@@ -107,6 +107,9 @@ enum Commands {
/// Output the preset to a file instead of stdout /// Output the preset to a file instead of stdout
#[clap(short, long, conflicts_with = "list")] #[clap(short, long, conflicts_with = "list")]
output: Option<PathBuf>, output: Option<PathBuf>,
/// Forcibly overwrite the output file if it already exists
#[clap(short, long, requires = "output")]
force: bool,
/// List out all preset names /// List out all preset names
#[clap(short, long)] #[clap(short, long)]
list: bool, list: bool,
@@ -251,7 +254,12 @@ fn main() {
print::module(&module_name, properties); print::module(&module_name, properties);
} }
} }
Commands::Preset { name, list, output } => print::preset_command(name, output, list), Commands::Preset {
name,
list,
output,
force,
} => print::preset_command(name, output, force, list),
Commands::Config { name, value } => { Commands::Config { name, value } => {
let context = Context::default(); let context = Context::default();
if let Some(name) = name { if let Some(name) = name {
+20 -48
View File
@@ -3,8 +3,8 @@ use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use chrono::DateTime;
use ini::Ini; use ini::Ini;
use jiff::{Timestamp, Zoned};
use serde_json as json; use serde_json as json;
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
@@ -149,7 +149,7 @@ fn get_credentials_duration(
.find_map(|env_var| context.get_env(env_var)) .find_map(|env_var| context.get_env(env_var))
{ {
// get expiration from environment variables // get expiration from environment variables
chrono::DateTime::parse_from_rfc3339(&expiration_date).ok() expiration_date.parse::<Timestamp>().ok()
} else if let Some(section) = } else if let Some(section) =
get_creds(context, aws_creds).and_then(|creds| get_profile_creds(creds, aws_profile)) get_creds(context, aws_creds).and_then(|creds| get_profile_creds(creds, aws_profile))
{ {
@@ -158,7 +158,7 @@ fn get_credentials_duration(
expiration_keys expiration_keys
.iter() .iter()
.find_map(|expiration_key| section.get(expiration_key)) .find_map(|expiration_key| section.get(expiration_key))
.and_then(|expiration| DateTime::parse_from_rfc3339(expiration).ok()) .and_then(|expiration| expiration.parse::<Timestamp>().ok())
} else { } else {
// get expiration from cached SSO credentials // get expiration from cached SSO credentials
let config = get_config(context, aws_config)?; let config = get_config(context, aws_config)?;
@@ -172,10 +172,10 @@ fn get_credentials_duration(
let sso_cred_json: json::Value = let sso_cred_json: json::Value =
json::from_str(&crate::utils::read_file(&sso_cred_path).ok()?).ok()?; json::from_str(&crate::utils::read_file(&sso_cred_path).ok()?).ok()?;
let expires_at = sso_cred_json.get("expiresAt")?.as_str(); let expires_at = sso_cred_json.get("expiresAt")?.as_str();
DateTime::parse_from_rfc3339(expires_at?).ok() expires_at?.parse::<Timestamp>().ok()
}?; }?;
Some(expiration_date.timestamp() - chrono::Local::now().timestamp()) Some(expiration_date.as_second() - Zoned::now().timestamp().as_second())
} }
fn alias_name(name: Option<String>, aliases: &HashMap<String, &str>) -> Option<String> { fn alias_name(name: Option<String>, aliases: &HashMap<String, &str>) -> Option<String> {
@@ -332,6 +332,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::test::ModuleRenderer; use crate::test::ModuleRenderer;
use nu_ansi_term::Color; use nu_ansi_term::Color;
use std::fs::{File, create_dir}; use std::fs::{File, create_dir};
@@ -736,21 +737,16 @@ credential_process = /opt/bin/awscreds-retriever
#[test] #[test]
fn expiration_date_set() { fn expiration_date_set() {
use chrono::{DateTime, SecondsFormat, Utc};
let expiration_env_vars = ["AWS_SESSION_EXPIRATION", "AWS_CREDENTIAL_EXPIRATION"]; let expiration_env_vars = ["AWS_SESSION_EXPIRATION", "AWS_CREDENTIAL_EXPIRATION"];
for env_var in expiration_env_vars { for env_var in expiration_env_vars {
let now_plus_half_hour: DateTime<Utc> = let now_plus_half_hour =
DateTime::from_timestamp(chrono::Local::now().timestamp() + 1800, 0).unwrap(); Timestamp::from_second(Zoned::now().timestamp().as_second() + 1800).unwrap();
let actual = ModuleRenderer::new("aws") let actual = ModuleRenderer::new("aws")
.env("AWS_PROFILE", "astronauts") .env("AWS_PROFILE", "astronauts")
.env("AWS_REGION", "ap-northeast-2") .env("AWS_REGION", "ap-northeast-2")
.env("AWS_ACCESS_KEY_ID", "dummy") .env("AWS_ACCESS_KEY_ID", "dummy")
.env( .env(env_var, now_plus_half_hour.to_string())
env_var,
now_plus_half_hour.to_rfc3339_opts(SecondsFormat::Secs, true),
)
.collect(); .collect();
let possible_values = [ let possible_values = [
@@ -772,16 +768,14 @@ credential_process = /opt/bin/awscreds-retriever
#[test] #[test]
fn expiration_date_set_from_file() -> io::Result<()> { fn expiration_date_set_from_file() -> io::Result<()> {
use chrono::{DateTime, Utc};
let dir = tempfile::tempdir()?; let dir = tempfile::tempdir()?;
let credentials_path = dir.path().join("credentials"); let credentials_path = dir.path().join("credentials");
let mut file = File::create(&credentials_path)?; let mut file = File::create(&credentials_path)?;
let now_plus_half_hour: DateTime<Utc> = let now_plus_half_hour =
DateTime::from_timestamp(chrono::Local::now().timestamp() + 1800, 0).unwrap(); Timestamp::from_second(Zoned::now().timestamp().as_second() + 1800).unwrap();
let expiration_date = now_plus_half_hour.to_rfc3339_opts(chrono::SecondsFormat::Secs, true); let expiration_date = now_plus_half_hour.to_string();
let expiration_keys = ["expiration", "x_security_token_expires"]; let expiration_keys = ["expiration", "x_security_token_expires"];
for key in expiration_keys { for key in expiration_keys {
@@ -847,10 +841,7 @@ aws_secret_access_key=dummy
#[test] #[test]
fn expiration_date_set_expired() { fn expiration_date_set_expired() {
use chrono::{DateTime, SecondsFormat, Utc}; let now = Timestamp::from_second(Zoned::now().timestamp().as_second() - 1800).unwrap();
let now: DateTime<Utc> =
DateTime::from_timestamp(chrono::Local::now().timestamp() - 1800, 0).unwrap();
let symbol = "!!!"; let symbol = "!!!";
@@ -862,10 +853,7 @@ aws_secret_access_key=dummy
.env("AWS_PROFILE", "astronauts") .env("AWS_PROFILE", "astronauts")
.env("AWS_REGION", "ap-northeast-2") .env("AWS_REGION", "ap-northeast-2")
.env("AWS_ACCESS_KEY_ID", "dummy") .env("AWS_ACCESS_KEY_ID", "dummy")
.env( .env("AWS_SESSION_EXPIRATION", now.to_string())
"AWS_SESSION_EXPIRATION",
now.to_rfc3339_opts(SecondsFormat::Secs, true),
)
.collect(); .collect();
let expected = Some(format!( let expected = Some(format!(
"on {}", "on {}",
@@ -1056,8 +1044,6 @@ credential_process = /opt/bin/awscreds-for-tests
#[test] #[test]
fn sso_legacy_set() -> io::Result<()> { fn sso_legacy_set() -> io::Result<()> {
use chrono::{DateTime, SecondsFormat, Utc};
let (module_renderer, dir) = ModuleRenderer::new_with_home("aws")?; let (module_renderer, dir) = ModuleRenderer::new_with_home("aws")?;
std::fs::create_dir_all(dir.path().join(".aws/sso/cache"))?; std::fs::create_dir_all(dir.path().join(".aws/sso/cache"))?;
@@ -1080,16 +1066,10 @@ sso_role_name = <AWS-ROLE-NAME>
.join(".aws/sso/cache/a47a4e57aecc96b31b4f083543924bd6f828e65a.json"), .join(".aws/sso/cache/a47a4e57aecc96b31b4f083543924bd6f828e65a.json"),
)?; )?;
let one_second_ago: DateTime<Utc> = let one_second_ago =
DateTime::from_timestamp(chrono::Local::now().timestamp() - 1, 0).unwrap(); Timestamp::from_second(Zoned::now().timestamp().as_second() - 1).unwrap();
file.write_all( file.write_all(format!(r#"{{"expiresAt": "{one_second_ago}"}}"#).as_bytes())?;
format!(
r#"{{"expiresAt": "{}"}}"#,
one_second_ago.to_rfc3339_opts(SecondsFormat::Secs, true)
)
.as_bytes(),
)?;
file.sync_all()?; file.sync_all()?;
let actual = module_renderer.collect(); let actual = module_renderer.collect();
@@ -1104,8 +1084,6 @@ sso_role_name = <AWS-ROLE-NAME>
#[test] #[test]
fn sso_set() -> io::Result<()> { fn sso_set() -> io::Result<()> {
use chrono::{DateTime, SecondsFormat, Utc};
let (module_renderer, dir) = ModuleRenderer::new_with_home("aws")?; let (module_renderer, dir) = ModuleRenderer::new_with_home("aws")?;
std::fs::create_dir_all(dir.path().join(".aws/sso/cache"))?; std::fs::create_dir_all(dir.path().join(".aws/sso/cache"))?;
@@ -1134,16 +1112,10 @@ sso_registration_scopes = sso:account:access
.join(".aws/sso/cache/7505d64a54e061b7acd54ccd58b49dc43500b635.json"), .join(".aws/sso/cache/7505d64a54e061b7acd54ccd58b49dc43500b635.json"),
)?; )?;
let one_second_ago: DateTime<Utc> = let one_second_ago =
DateTime::from_timestamp(chrono::Local::now().timestamp() - 1, 0).unwrap(); Timestamp::from_second(Zoned::now().timestamp().as_second() - 1).unwrap();
cache_file.write_all( cache_file.write_all(format!(r#"{{"expiresAt": "{one_second_ago}"}}"#).as_bytes())?;
format!(
r#"{{"expiresAt": "{}"}}"#,
one_second_ago.to_rfc3339_opts(SecondsFormat::Secs, true)
)
.as_bytes(),
)?;
cache_file.sync_all()?; cache_file.sync_all()?;
let actual = module_renderer let actual = module_renderer
+68 -4
View File
@@ -116,10 +116,17 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
.and_then(|(_, domain)| domain) .and_then(|(_, domain)| domain)
.map(Cow::Borrowed) .map(Cow::Borrowed)
.map(Ok), .map(Ok),
"region" => gcloud_context "region" => context
.get_region() .get_env("CLOUDSDK_COMPUTE_REGION")
.map(|region| config.region_aliases.get(region).copied().unwrap_or(region)) .map(Cow::Owned)
.map(Cow::Borrowed) .or_else(|| gcloud_context.get_region().map(Cow::Borrowed))
.map(|region| {
config
.region_aliases
.get(region.as_ref())
.copied()
.map_or(region, Cow::Borrowed)
})
.map(Ok), .map(Ok),
"project" => context "project" => context
.get_env("CLOUDSDK_CORE_PROJECT") .get_env("CLOUDSDK_CORE_PROJECT")
@@ -451,6 +458,63 @@ project = very-long-project-name
dir.close() dir.close()
} }
#[test]
fn region_set_in_env() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let active_config_path = dir.path().join("active_config");
let mut active_config_file = File::create(active_config_path)?;
active_config_file.write_all(b"default")?;
create_dir(dir.path().join("configurations"))?;
let config_default_path = dir.path().join("configurations").join("config_default");
let mut config_default_file = File::create(config_default_path)?;
config_default_file.write_all(
b"\
[compute]
region = us-central1
",
)?;
let actual = ModuleRenderer::new("gcloud")
.env("CLOUDSDK_COMPUTE_REGION", "europe-west2")
.env("CLOUDSDK_CONFIG", dir.path().to_string_lossy())
.config(toml::toml! {
[gcloud]
format = "on [$symbol$region]($style) "
})
.collect();
let expected = Some(format!(
"on {} ",
Color::Blue.bold().paint("☁️ europe-west2")
));
assert_eq!(actual, expected);
dir.close()
}
#[test]
fn region_set_in_env_with_alias() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let active_config_path = dir.path().join("active_config");
let mut active_config_file = File::create(active_config_path)?;
active_config_file.write_all(b"default")?;
let actual = ModuleRenderer::new("gcloud")
.env("CLOUDSDK_COMPUTE_REGION", "europe-west2")
.env("CLOUDSDK_CONFIG", dir.path().to_string_lossy())
.config(toml::toml! {
[gcloud]
format = "on [$symbol$region]($style) "
[gcloud.region_aliases]
europe-west2 = "ew2"
})
.collect();
let expected = Some(format!("on {} ", Color::Blue.bold().paint("☁️ ew2")));
assert_eq!(actual, expected);
dir.close()
}
#[test] #[test]
fn region_not_set_with_display_region() -> io::Result<()> { fn region_not_set_with_display_region() -> io::Result<()> {
let dir = tempfile::tempdir()?; let dir = tempfile::tempdir()?;
+50 -13
View File
@@ -75,24 +75,16 @@ fn get_state_description<'a>(
current: None, current: None,
total: None, total: None,
}), }),
InProgress::ApplyMailbox => Some(StateDescription { InProgress::ApplyMailbox => Some(describe_rebase_apply(repo, config.am)),
label: config.am, InProgress::ApplyMailboxRebase => Some(describe_rebase_apply(repo, config.am_or_rebase)),
current: None,
total: None,
}),
InProgress::ApplyMailboxRebase => Some(StateDescription {
label: config.am_or_rebase,
current: None,
total: None,
}),
InProgress::Rebase | InProgress::RebaseInteractive => { InProgress::Rebase | InProgress::RebaseInteractive => {
Some(describe_rebase(repo, config.rebase)) Some(describe_rebase_apply(repo, config.rebase))
} }
} }
} }
// TODO: Use future gitoxide API to get the state of the rebase // TODO: Use future gitoxide API to get the state of the rebase
fn describe_rebase<'a>(repo: &'a Repo, rebase_config: &'a str) -> StateDescription<'a> { fn describe_rebase_apply<'a>(repo: &'a Repo, state_label: &'a str) -> StateDescription<'a> {
/* /*
* Sadly, libgit2 seems to have some issues with reading the state of * Sadly, libgit2 seems to have some issues with reading the state of
* interactive rebases. So, instead, we'll poke a few of the .git files * interactive rebases. So, instead, we'll poke a few of the .git files
@@ -134,7 +126,7 @@ fn describe_rebase<'a>(repo: &'a Repo, rebase_config: &'a str) -> StateDescripti
}; };
StateDescription { StateDescription {
label: rebase_config, label: state_label,
current, current,
total, total,
} }
@@ -186,6 +178,33 @@ mod tests {
repo_dir.close() repo_dir.close()
} }
#[test]
fn shows_applying_mailbox_progress() -> io::Result<()> {
let repo_dir = create_repo_with_conflict()?;
let path = repo_dir.path();
let patch_path = path.join("other-branch.patch");
let patch =
run_git_cmd_with_output(["format-patch", "-1", "other-branch", "--stdout"], path)?;
write_file(
&patch_path,
String::from_utf8(patch.stdout)
.map_err(|error| Error::new(ErrorKind::InvalidData, error))?,
)?;
run_git_cmd(
["am", patch_path.to_str().expect("Path was not UTF-8")],
Some(path),
false,
)?;
let actual = ModuleRenderer::new("git_state").path(path).collect();
let expected = Some(format!("({}) ", Color::Yellow.bold().paint("AM 1/1")));
assert_eq!(expected, actual);
repo_dir.close()
}
#[test] #[test]
fn shows_merging() -> io::Result<()> { fn shows_merging() -> io::Result<()> {
let repo_dir = create_repo_with_conflict()?; let repo_dir = create_repo_with_conflict()?;
@@ -274,6 +293,24 @@ mod tests {
} }
} }
fn run_git_cmd_with_output<A, S>(args: A, dir: &Path) -> io::Result<std::process::Output>
where
A: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let output = create_command("git")?
.args(args)
.current_dir(dir)
.stderr(Stdio::null())
.output()?;
if output.status.success() {
Ok(output)
} else {
Err(Error::from(ErrorKind::Other))
}
}
fn create_repo_with_conflict() -> io::Result<tempfile::TempDir> { fn create_repo_with_conflict() -> io::Result<tempfile::TempDir> {
let repo_dir = tempfile::tempdir()?; let repo_dir = tempfile::tempdir()?;
let path = repo_dir.path(); let path = repo_dir.path();
+22 -8
View File
@@ -12,18 +12,19 @@ use crate::{
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module = context.new_module("maven"); let mut module = context.new_module("maven");
let config = MavenConfig::try_load(module.config); let config = MavenConfig::try_load(module.config);
let is_maven_project = context let wrapper_properties = get_wrapper_properties_file(context, config.recursive);
.try_begin_scan()? let is_maven_project = wrapper_properties.is_some()
.set_files(&config.detect_files) || context
.set_extensions(&config.detect_extensions) .try_begin_scan()?
.set_folders(&config.detect_folders) .set_files(&config.detect_files)
.is_match(); .set_extensions(&config.detect_extensions)
.set_folders(&config.detect_folders)
.is_match();
if !is_maven_project { if !is_maven_project {
return None; return None;
} }
let wrapper_properties = get_wrapper_properties_file(context, config.recursive);
let parsed = StringFormatter::new(config.format).and_then(|formatter| { let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter formatter
.map_meta(|var, _| match var { .map_meta(|var, _| match var {
@@ -123,6 +124,20 @@ mod tests {
dir.close() dir.close()
} }
#[test]
fn folder_with_maven_config_does_not_trigger_module() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let maven_config = dir.path().join(".mvn").join("maven.config");
fs::create_dir_all(maven_config.parent().unwrap())?;
File::create(maven_config)?.sync_all()?;
let actual = ModuleRenderer::new("maven").path(dir.path()).collect();
let expected = None;
assert_eq!(expected, actual);
dir.close()
}
#[test] #[test]
fn folder_with_maven_wrapper_properties() -> io::Result<()> { fn folder_with_maven_wrapper_properties() -> io::Result<()> {
let dir = tempfile::tempdir()?; let dir = tempfile::tempdir()?;
@@ -132,7 +147,6 @@ mod tests {
.join("wrapper") .join("wrapper")
.join("maven-wrapper.properties"); .join("maven-wrapper.properties");
fs::create_dir_all(properties.parent().unwrap())?; fs::create_dir_all(properties.parent().unwrap())?;
File::create(dir.path().join("pom.xml"))?.sync_all()?;
let mut file = File::create(properties)?; let mut file = File::create(properties)?;
file.write_all( file.write_all(
b"\ b"\
+20 -2
View File
@@ -45,6 +45,7 @@ impl NixShellType {
/// The module will use the `$IN_NIX_SHELL` and `$name` environment variable to /// The module will use the `$IN_NIX_SHELL` and `$name` environment variable to
/// determine if it's inside a nix-shell and the name of it. /// determine if it's inside a nix-shell and the name of it.
/// ///
///
/// The following options are available: /// The following options are available:
/// - `impure_msg` (string) // change the impure msg /// - `impure_msg` (string) // change the impure msg
/// - `pure_msg` (string) // change the pure msg /// - `pure_msg` (string) // change the pure msg
@@ -57,11 +58,12 @@ impl NixShellType {
/// - impure // $name == "" in an impure nix-shell /// - impure // $name == "" in an impure nix-shell
/// - unknown (name) // $name == "name" in an unknown nix-shell /// - unknown (name) // $name == "name" in an unknown nix-shell
/// - unknown // $name == "" in an unknown nix-shell /// - unknown // $name == "" in an unknown nix-shell
///
/// When using Lix 2.95+, will also have $level from `$NIX_SHELL_LEVEL`
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module = context.new_module("nix_shell"); let mut module = context.new_module("nix_shell");
let config: NixShellConfig = NixShellConfig::try_load(module.config); let config: NixShellConfig = NixShellConfig::try_load(module.config);
let shell_name = context.get_env("name");
let shell_type = NixShellType::detect_shell_type(config.heuristic, context)?; let shell_type = NixShellType::detect_shell_type(config.heuristic, context)?;
let shell_type_format = match shell_type { let shell_type_format = match shell_type {
NixShellType::Pure => config.pure_msg, NixShellType::Pure => config.pure_msg,
@@ -81,7 +83,8 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
_ => None, _ => None,
}) })
.map(|variable| match variable { .map(|variable| match variable {
"name" => shell_name.as_ref().map(Ok), "name" => context.get_env("name").map(Ok),
"level" => context.get_env("NIX_SHELL_LEVEL").map(Ok),
_ => None, _ => None,
}) })
.parse(None, Some(context)) .parse(None, Some(context))
@@ -226,4 +229,19 @@ mod tests {
assert_eq!(expected, actual); assert_eq!(expected, actual);
} }
#[test]
fn nix_shell_level() {
let actual = ModuleRenderer::new("nix_shell")
.env("IN_NIX_SHELL", "impure")
.env("NIX_SHELL_LEVEL", "3")
.config(toml::toml! {
[nix_shell]
format = "via [$symbol$state( \\($name\\)) $level]($style) "
})
.collect();
let expected = Some(format!("via {} ", Color::Blue.bold().paint("❄️ impure 3")));
assert_eq!(expected, actual);
}
} }
+40
View File
@@ -44,6 +44,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
}) })
.map(|variable| match variable { .map(|variable| match variable {
"environment" => pixi_environment_name.clone().map(Ok), "environment" => pixi_environment_name.clone().map(Ok),
"project_name" => context.get_env("PIXI_PROJECT_NAME").map(Ok),
"version" => { "version" => {
let pixi_version = get_pixi_version(context, &config)?; let pixi_version = get_pixi_version(context, &config)?;
VersionFormatter::format_module_version( VersionFormatter::format_module_version(
@@ -125,6 +126,45 @@ mod tests {
assert_eq!(expected, actual); assert_eq!(expected, actual);
} }
#[test]
fn project_name_set() {
let actual = ModuleRenderer::new("pixi")
.env("PIXI_ENVIRONMENT_NAME", "py312")
.env("PIXI_PROJECT_NAME", "my-project")
.config(toml::toml! {
[pixi]
format = "via [$symbol($version )(\\($environment\\) )(\\[$project_name\\] )]($style)"
})
.collect();
let expected = Some(format!(
"via {}",
Color::Yellow
.bold()
.paint("🧚 v0.33.0 (py312) [my-project] ")
));
assert_eq!(expected, actual);
}
#[test]
fn project_name_not_set() {
let actual = ModuleRenderer::new("pixi")
.env("PIXI_ENVIRONMENT_NAME", "py312")
.config(toml::toml! {
[pixi]
format = "via [$symbol($version )(\\($environment\\) )(\\[$project_name\\] )]($style)"
})
.collect();
let expected = Some(format!(
"via {}",
Color::Yellow.bold().paint("🧚 v0.33.0 (py312) ")
));
assert_eq!(expected, actual);
}
#[test] #[test]
fn folder_with_pixi_toml() -> io::Result<()> { fn folder_with_pixi_toml() -> io::Result<()> {
let dir = tempfile::tempdir()?; let dir = tempfile::tempdir()?;
+258 -111
View File
@@ -1,7 +1,12 @@
use chrono::{DateTime, FixedOffset, Local, NaiveTime, Utc}; use jiff::{
Timestamp, Zoned,
civil::Time,
tz::{Offset, TimeZone},
};
use super::{Context, Module, ModuleConfig}; use super::{Context, Module, ModuleConfig};
use crate::configs::time::TimeConfig; use crate::config::Either;
use crate::configs::time::{TimeConfig, TimezoneWrapper};
use crate::formatter::StringFormatter; use crate::formatter::StringFormatter;
/// Outputs the current time /// Outputs the current time
@@ -17,7 +22,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// Hide prompt if current time is not inside time_range // Hide prompt if current time is not inside time_range
let (display_start, display_end) = parse_time_range(config.time_range); let (display_start, display_end) = parse_time_range(config.time_range);
let time_now = Local::now().time(); let time_now = Zoned::now().time();
if !is_inside_time_range(time_now, display_start, display_end) { if !is_inside_time_range(time_now, display_start, display_end) {
return None; return None;
} }
@@ -27,17 +32,26 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
log::trace!("Timer module is enabled with format string: {time_format}"); log::trace!("Timer module is enabled with format string: {time_format}");
let formatted_time_string = if config.utc_time_offset != "local" { let formatted_time_string = match &config.utc_time_offset {
create_offset_time_string(Utc::now(), config.utc_time_offset, time_format).unwrap_or_else( Either::First(TimezoneWrapper(tz)) => {
|_| { // Use IANA timezone name
log::warn!( let target_time = Timestamp::now().to_zoned(tz.clone());
"Invalid utc_time_offset configuration provided! Falling back to \"local\"." format_time_fixed_offset(time_format, target_time)
); }
format_time(time_format, Local::now()) Either::Second("local") => {
}, // Use local timezone
) format_time(time_format, Zoned::now())
} else { }
format_time(time_format, Local::now()) Either::Second(utc_time_offset) => {
// Use numeric offset
create_offset_time_string(Timestamp::now(), utc_time_offset, time_format)
.unwrap_or_else(|_| {
log::warn!(
"Invalid utc_time_offset configuration provided! Falling back to \"local\"."
);
format_time(time_format, Zoned::now())
})
}
}; };
let parsed = StringFormatter::new(config.format).and_then(|formatter| { let parsed = StringFormatter::new(config.format).and_then(|formatter| {
@@ -65,10 +79,10 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
fn create_offset_time_string( fn create_offset_time_string(
utc_time: DateTime<Utc>, utc_time: Timestamp,
utc_time_offset_str: &str, utc_time_offset_str: &str,
time_format: &str, time_format: &str,
) -> Result<String, &'static str> { ) -> Result<String, String> {
// Using floats to allow 30/45 minute offsets: https://www.timeanddate.com/time/time-zones-interesting.html // Using floats to allow 30/45 minute offsets: https://www.timeanddate.com/time/time-zones-interesting.html
let utc_time_offset_in_hours = utc_time_offset_str.parse::<f32>().unwrap_or( let utc_time_offset_in_hours = utc_time_offset_str.parse::<f32>().unwrap_or(
// Passing out of range value to force falling back to "local" // Passing out of range value to force falling back to "local"
@@ -76,38 +90,34 @@ fn create_offset_time_string(
); );
if utc_time_offset_in_hours < 24_f32 && utc_time_offset_in_hours > -24_f32 { if utc_time_offset_in_hours < 24_f32 && utc_time_offset_in_hours > -24_f32 {
let utc_offset_in_seconds: i32 = (utc_time_offset_in_hours * 3600_f32) as i32; let utc_offset_in_seconds: i32 = (utc_time_offset_in_hours * 3600_f32) as i32;
let Some(timezone_offset) = FixedOffset::east_opt(utc_offset_in_seconds) else { let timezone_offset = Offset::from_seconds(utc_offset_in_seconds)
return Err("Invalid offset"); .map_err(|err| format!("Invalid timezone offset: {err:?}"))?;
}; let tz = TimeZone::fixed(timezone_offset);
log::trace!("Target timezone offset is {timezone_offset}"); log::trace!("Target timezone offset is {timezone_offset}");
let target_time = utc_time.with_timezone(&timezone_offset); let target_time = utc_time.to_zoned(tz);
log::trace!("Time in target timezone now is {target_time}"); log::trace!("Time in target timezone now is {target_time}");
Ok(format_time_fixed_offset(time_format, target_time)) Ok(format_time_fixed_offset(time_format, target_time))
} else { } else {
Err("Invalid timezone offset.") Err("Invalid timezone offset.".to_string())
} }
} }
/// Format a given time into the given string. This function should be referentially /// Format a given time into the given string. This function should be referentially
/// transparent, which makes it easy to test (unlike anything involving the actual time) /// transparent, which makes it easy to test (unlike anything involving the actual time)
fn format_time(time_format: &str, local_time: DateTime<Local>) -> String { fn format_time(time_format: &str, local_time: Zoned) -> String {
local_time.format(time_format).to_string() local_time.strftime(time_format).to_string()
} }
fn format_time_fixed_offset(time_format: &str, utc_time: DateTime<FixedOffset>) -> String { fn format_time_fixed_offset(time_format: &str, zoned_time: Zoned) -> String {
utc_time.format(time_format).to_string() zoned_time.strftime(time_format).to_string()
} }
/// Returns true if `time_now` is between `time_start` and `time_end`. /// Returns true if `time_now` is between `time_start` and `time_end`.
/// If one of these values is not given, then it is ignored. /// If one of these values is not given, then it is ignored.
/// It also handles cases where `time_start` and `time_end` have a midnight in between /// It also handles cases where `time_start` and `time_end` have a midnight in between
fn is_inside_time_range( fn is_inside_time_range(time_now: Time, time_start: Option<Time>, time_end: Option<Time>) -> bool {
time_now: NaiveTime,
time_start: Option<NaiveTime>,
time_end: Option<NaiveTime>,
) -> bool {
match (time_start, time_end) { match (time_start, time_end) {
(None, None) => true, (None, None) => true,
(Some(i), None) => time_now > i, (Some(i), None) => time_now > i,
@@ -127,7 +137,7 @@ fn is_inside_time_range(
/// ///
/// If one of the ranges is invalid or not provided, then the corresponding field in the output /// If one of the ranges is invalid or not provided, then the corresponding field in the output
/// tuple is None /// tuple is None
fn parse_time_range(time_range: &str) -> (Option<NaiveTime>, Option<NaiveTime>) { fn parse_time_range(time_range: &str) -> (Option<Time>, Option<Time>) {
let value = String::from(time_range); let value = String::from(time_range);
// Check if there is exactly one hyphen, and fail otherwise // Check if there is exactly one hyphen, and fail otherwise
@@ -140,8 +150,8 @@ fn parse_time_range(time_range: &str) -> (Option<NaiveTime>, Option<NaiveTime>)
let end = &end[1..]; let end = &end[1..];
// Parse the ranges // Parse the ranges
let start_time = NaiveTime::parse_from_str(start, "%H:%M:%S").ok(); let start_time = start.parse::<Time>().ok();
let end_time = NaiveTime::parse_from_str(end, "%H:%M:%S").ok(); let end_time = end.parse::<Time>().ok();
(start_time, end_time) (start_time, end_time)
} }
@@ -152,140 +162,151 @@ tests become extra important */
mod tests { mod tests {
use super::*; use super::*;
use crate::test::ModuleRenderer; use crate::test::ModuleRenderer;
use chrono::offset::TimeZone; use jiff::civil::date;
const FMT_12: &str = "%r"; const FMT_12: &str = "%r";
const FMT_24: &str = "%T"; const FMT_24: &str = "%T";
#[test] #[test]
fn test_midnight_12hr() { fn test_midnight_12hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 0, 0, 0).unwrap(); let time = date(2014, 7, 8)
.at(0, 0, 0, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_12, time); let formatted = format_time(FMT_12, time);
assert_eq!(formatted, "12:00:00 AM"); assert_eq!(formatted, "12:00:00 AM");
} }
#[test] #[test]
fn test_midnight_24hr() { fn test_midnight_24hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 0, 0, 0).unwrap(); let time = date(2014, 7, 8)
.at(0, 0, 0, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_24, time); let formatted = format_time(FMT_24, time);
assert_eq!(formatted, "00:00:00"); assert_eq!(formatted, "00:00:00");
} }
#[test] #[test]
fn test_noon_12hr() { fn test_noon_12hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 12, 0, 0).unwrap(); let time = date(2014, 7, 8)
.at(12, 0, 0, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_12, time); let formatted = format_time(FMT_12, time);
assert_eq!(formatted, "12:00:00 PM"); assert_eq!(formatted, "12:00:00 PM");
} }
#[test] #[test]
fn test_noon_24hr() { fn test_noon_24hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 12, 0, 0).unwrap(); let time = date(2014, 7, 8)
.at(12, 0, 0, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_24, time); let formatted = format_time(FMT_24, time);
assert_eq!(formatted, "12:00:00"); assert_eq!(formatted, "12:00:00");
} }
#[test] #[test]
fn test_arbtime_12hr() { fn test_arbtime_12hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_12, time); let formatted = format_time(FMT_12, time);
assert_eq!(formatted, "03:36:47 PM"); assert_eq!(formatted, "3:36:47 PM");
} }
#[test] #[test]
fn test_arbtime_24hr() { fn test_arbtime_24hr() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time(FMT_24, time); let formatted = format_time(FMT_24, time);
assert_eq!(formatted, "15:36:47"); assert_eq!(formatted, "15:36:47");
} }
#[test] #[test]
fn test_format_with_paren() { fn test_format_with_paren() {
let time = Local.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::system())
.unwrap();
let formatted = format_time("[%T]", time); let formatted = format_time("[%T]", time);
assert_eq!(formatted, "[15:36:47]"); assert_eq!(formatted, "[15:36:47]");
} }
#[test] #[test]
fn test_midnight_12hr_fixed_offset() { fn test_midnight_12hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap(); let timezone_offset = Offset::from_seconds(0).unwrap();
let time = Utc let tz = TimeZone::fixed(timezone_offset);
.with_ymd_and_hms(2014, 7, 8, 0, 0, 0) let time = date(2014, 7, 8).at(0, 0, 0, 0).to_zoned(tz).unwrap();
.unwrap()
.with_timezone(&timezone_offset);
let formatted = format_time_fixed_offset(FMT_12, time); let formatted = format_time_fixed_offset(FMT_12, time);
assert_eq!(formatted, "12:00:00 AM"); assert_eq!(formatted, "12:00:00 AM");
} }
#[test] #[test]
fn test_midnight_24hr_fixed_offset() { fn test_midnight_24hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap(); let timezone_offset = Offset::from_seconds(0).unwrap();
let time = Utc let tz = TimeZone::fixed(timezone_offset);
.with_ymd_and_hms(2014, 7, 8, 0, 0, 0) let time = date(2014, 7, 8).at(0, 0, 0, 0).to_zoned(tz).unwrap();
.unwrap()
.with_timezone(&timezone_offset);
let formatted = format_time_fixed_offset(FMT_24, time); let formatted = format_time_fixed_offset(FMT_24, time);
assert_eq!(formatted, "00:00:00"); assert_eq!(formatted, "00:00:00");
} }
#[test] #[test]
fn test_noon_12hr_fixed_offset() { fn test_noon_12hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap(); let timezone_offset = Offset::from_seconds(0).unwrap();
let time = Utc let tz = TimeZone::fixed(timezone_offset);
.with_ymd_and_hms(2014, 7, 8, 12, 0, 0) let time = date(2014, 7, 8).at(12, 0, 0, 0).to_zoned(tz).unwrap();
.unwrap()
.with_timezone(&timezone_offset);
let formatted = format_time_fixed_offset(FMT_12, time); let formatted = format_time_fixed_offset(FMT_12, time);
assert_eq!(formatted, "12:00:00 PM"); assert_eq!(formatted, "12:00:00 PM");
} }
#[test] #[test]
fn test_noon_24hr_fixed_offset() { fn test_noon_24hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap(); let timezone_offset = Offset::from_seconds(0).unwrap();
let time = Utc let tz = TimeZone::fixed(timezone_offset);
.with_ymd_and_hms(2014, 7, 8, 12, 0, 0) let time = date(2014, 7, 8).at(12, 0, 0, 0).to_zoned(tz).unwrap();
.unwrap()
.with_timezone(&timezone_offset);
let formatted = format_time_fixed_offset(FMT_24, time); let formatted = format_time_fixed_offset(FMT_24, time);
assert_eq!(formatted, "12:00:00"); assert_eq!(formatted, "12:00:00");
} }
#[test] #[test]
fn test_arbtime_12hr_fixed_offset() { fn test_arbtime_12hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap(); let timezone_offset = Offset::from_seconds(0).unwrap();
let time = Utc let tz = TimeZone::fixed(timezone_offset);
.with_ymd_and_hms(2014, 7, 8, 15, 36, 47) let time = date(2014, 7, 8).at(15, 36, 47, 0).to_zoned(tz).unwrap();
.unwrap()
.with_timezone(&timezone_offset);
let formatted = format_time_fixed_offset(FMT_12, time); let formatted = format_time_fixed_offset(FMT_12, time);
assert_eq!(formatted, "03:36:47 PM"); assert_eq!(formatted, "3:36:47 PM");
} }
#[test] #[test]
fn test_arbtime_24hr_fixed_offset() { fn test_arbtime_24hr_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap(); let timezone_offset = Offset::from_seconds(0).unwrap();
let time = Utc let tz = TimeZone::fixed(timezone_offset);
.with_ymd_and_hms(2014, 7, 8, 15, 36, 47) let time = date(2014, 7, 8).at(15, 36, 47, 0).to_zoned(tz).unwrap();
.unwrap()
.with_timezone(&timezone_offset);
let formatted = format_time_fixed_offset(FMT_24, time); let formatted = format_time_fixed_offset(FMT_24, time);
assert_eq!(formatted, "15:36:47"); assert_eq!(formatted, "15:36:47");
} }
#[test] #[test]
fn test_format_with_paren_fixed_offset() { fn test_format_with_paren_fixed_offset() {
let timezone_offset = FixedOffset::east_opt(0).unwrap(); let timezone_offset = Offset::from_seconds(0).unwrap();
let time = Utc let tz = TimeZone::fixed(timezone_offset);
.with_ymd_and_hms(2014, 7, 8, 15, 36, 47) let time = date(2014, 7, 8).at(15, 36, 47, 0).to_zoned(tz).unwrap();
.unwrap()
.with_timezone(&timezone_offset);
let formatted = format_time_fixed_offset("[%T]", time); let formatted = format_time_fixed_offset("[%T]", time);
assert_eq!(formatted, "[15:36:47]"); assert_eq!(formatted, "[15:36:47]");
} }
#[test] #[test]
fn test_create_formatted_time_string_with_minus_3() { fn test_create_formatted_time_string_with_minus_3() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "-3"; let utc_time_offset_str = "-3";
let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap(); let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap();
@@ -294,34 +315,50 @@ mod tests {
#[test] #[test]
fn test_create_formatted_time_string_with_plus_5() { fn test_create_formatted_time_string_with_plus_5() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "+5"; let utc_time_offset_str = "+5";
let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap(); let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap();
assert_eq!(actual, "08:36:47 PM"); assert_eq!(actual, "8:36:47 PM");
} }
#[test] #[test]
fn test_create_formatted_time_string_with_plus_9_30() { fn test_create_formatted_time_string_with_plus_9_30() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "+9.5"; let utc_time_offset_str = "+9.5";
let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap(); let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap();
assert_eq!(actual, "01:06:47 AM"); assert_eq!(actual, "1:06:47 AM");
} }
#[test] #[test]
fn test_create_formatted_time_string_with_plus_5_45() { fn test_create_formatted_time_string_with_plus_5_45() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "+5.75"; let utc_time_offset_str = "+5.75";
let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap(); let actual = create_offset_time_string(utc_time, utc_time_offset_str, FMT_12).unwrap();
assert_eq!(actual, "09:21:47 PM"); assert_eq!(actual, "9:21:47 PM");
} }
#[test] #[test]
fn test_create_formatted_time_string_with_plus_24() { fn test_create_formatted_time_string_with_plus_24() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "+24"; let utc_time_offset_str = "+24";
create_offset_time_string(utc_time, utc_time_offset_str, FMT_12) create_offset_time_string(utc_time, utc_time_offset_str, FMT_12)
@@ -330,7 +367,11 @@ mod tests {
#[test] #[test]
fn test_create_formatted_time_string_with_minus_24() { fn test_create_formatted_time_string_with_minus_24() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "-24"; let utc_time_offset_str = "-24";
create_offset_time_string(utc_time, utc_time_offset_str, FMT_12) create_offset_time_string(utc_time, utc_time_offset_str, FMT_12)
@@ -339,7 +380,11 @@ mod tests {
#[test] #[test]
fn test_create_formatted_time_string_with_plus_9001() { fn test_create_formatted_time_string_with_plus_9001() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "+9001"; let utc_time_offset_str = "+9001";
create_offset_time_string(utc_time, utc_time_offset_str, FMT_12) create_offset_time_string(utc_time, utc_time_offset_str, FMT_12)
@@ -348,7 +393,11 @@ mod tests {
#[test] #[test]
fn test_create_formatted_time_string_with_minus_4242() { fn test_create_formatted_time_string_with_minus_4242() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "-4242"; let utc_time_offset_str = "-4242";
create_offset_time_string(utc_time, utc_time_offset_str, FMT_12) create_offset_time_string(utc_time, utc_time_offset_str, FMT_12)
@@ -357,7 +406,11 @@ mod tests {
#[test] #[test]
fn test_create_formatted_time_string_with_invalid_string() { fn test_create_formatted_time_string_with_invalid_string() {
let utc_time: DateTime<Utc> = Utc.with_ymd_and_hms(2014, 7, 8, 15, 36, 47).unwrap(); let utc_time = date(2014, 7, 8)
.at(15, 36, 47, 0)
.to_zoned(TimeZone::UTC)
.unwrap()
.timestamp();
let utc_time_offset_str = "completely wrong config"; let utc_time_offset_str = "completely wrong config";
create_offset_time_string(utc_time, utc_time_offset_str, FMT_12) create_offset_time_string(utc_time, utc_time_offset_str, FMT_12)
@@ -379,7 +432,7 @@ mod tests {
assert_eq!( assert_eq!(
parse_time_range(time_range), parse_time_range(time_range),
(Some(NaiveTime::from_hms_opt(10, 00, 00).unwrap()), None) (Some(Time::new(10, 0, 0, 0).unwrap()), None)
); );
} }
@@ -389,7 +442,7 @@ mod tests {
assert_eq!( assert_eq!(
parse_time_range(time_range), parse_time_range(time_range),
(None, Some(NaiveTime::from_hms_opt(22, 00, 00).unwrap())) (None, Some(Time::new(22, 0, 0, 0).unwrap()))
); );
} }
@@ -400,8 +453,8 @@ mod tests {
assert_eq!( assert_eq!(
parse_time_range(time_range), parse_time_range(time_range),
( (
Some(NaiveTime::from_hms_opt(10, 00, 00).unwrap()), Some(Time::new(10, 0, 0, 0).unwrap()),
Some(NaiveTime::from_hms_opt(16, 00, 00).unwrap()) Some(Time::new(16, 0, 0, 0).unwrap())
) )
); );
} }
@@ -410,16 +463,16 @@ mod tests {
fn test_is_inside_time_range_with_no_range() { fn test_is_inside_time_range_with_no_range() {
let time_start = None; let time_start = None;
let time_end = None; let time_end = None;
let time_now = NaiveTime::from_hms_opt(10, 00, 00).unwrap(); let time_now = Time::new(10, 0, 0, 0).unwrap();
assert!(is_inside_time_range(time_now, time_start, time_end)); assert!(is_inside_time_range(time_now, time_start, time_end));
} }
#[test] #[test]
fn test_is_inside_time_range_with_start_range() { fn test_is_inside_time_range_with_start_range() {
let time_start = Some(NaiveTime::from_hms_opt(10, 00, 00).unwrap()); let time_start = Some(Time::new(10, 0, 0, 0).unwrap());
let time_now = NaiveTime::from_hms_opt(12, 00, 00).unwrap(); let time_now = Time::new(12, 0, 0, 0).unwrap();
let time_now2 = NaiveTime::from_hms_opt(8, 00, 00).unwrap(); let time_now2 = Time::new(8, 0, 0, 0).unwrap();
assert!(is_inside_time_range(time_now, time_start, None)); assert!(is_inside_time_range(time_now, time_start, None));
assert!(!is_inside_time_range(time_now2, time_start, None)); assert!(!is_inside_time_range(time_now2, time_start, None));
@@ -427,9 +480,9 @@ mod tests {
#[test] #[test]
fn test_is_inside_time_range_with_end_range() { fn test_is_inside_time_range_with_end_range() {
let time_end = Some(NaiveTime::from_hms_opt(16, 00, 00).unwrap()); let time_end = Some(Time::new(16, 0, 0, 0).unwrap());
let time_now = NaiveTime::from_hms_opt(15, 00, 00).unwrap(); let time_now = Time::new(15, 0, 0, 0).unwrap();
let time_now2 = NaiveTime::from_hms_opt(19, 00, 00).unwrap(); let time_now2 = Time::new(19, 0, 0, 0).unwrap();
assert!(is_inside_time_range(time_now, None, time_end)); assert!(is_inside_time_range(time_now, None, time_end));
assert!(!is_inside_time_range(time_now2, None, time_end)); assert!(!is_inside_time_range(time_now2, None, time_end));
@@ -437,11 +490,11 @@ mod tests {
#[test] #[test]
fn test_is_inside_time_range_with_complete_range() { fn test_is_inside_time_range_with_complete_range() {
let time_start = Some(NaiveTime::from_hms_opt(9, 00, 00).unwrap()); let time_start = Some(Time::new(9, 0, 0, 0).unwrap());
let time_end = Some(NaiveTime::from_hms_opt(18, 00, 00).unwrap()); let time_end = Some(Time::new(18, 0, 0, 0).unwrap());
let time_now = NaiveTime::from_hms_opt(3, 00, 00).unwrap(); let time_now = Time::new(3, 0, 0, 0).unwrap();
let time_now2 = NaiveTime::from_hms_opt(13, 00, 00).unwrap(); let time_now2 = Time::new(13, 0, 0, 0).unwrap();
let time_now3 = NaiveTime::from_hms_opt(20, 00, 00).unwrap(); let time_now3 = Time::new(20, 0, 0, 0).unwrap();
assert!(!is_inside_time_range(time_now, time_start, time_end)); assert!(!is_inside_time_range(time_now, time_start, time_end));
assert!(is_inside_time_range(time_now2, time_start, time_end)); assert!(is_inside_time_range(time_now2, time_start, time_end));
@@ -450,11 +503,11 @@ mod tests {
#[test] #[test]
fn test_is_inside_time_range_with_complete_range_passing_midnight() { fn test_is_inside_time_range_with_complete_range_passing_midnight() {
let time_start = Some(NaiveTime::from_hms_opt(19, 00, 00).unwrap()); let time_start = Some(Time::new(19, 0, 0, 0).unwrap());
let time_end = Some(NaiveTime::from_hms_opt(12, 00, 00).unwrap()); let time_end = Some(Time::new(12, 0, 0, 0).unwrap());
let time_now = NaiveTime::from_hms_opt(3, 00, 00).unwrap(); let time_now = Time::new(3, 0, 0, 0).unwrap();
let time_now2 = NaiveTime::from_hms_opt(13, 00, 00).unwrap(); let time_now2 = Time::new(13, 0, 0, 0).unwrap();
let time_now3 = NaiveTime::from_hms_opt(20, 00, 00).unwrap(); let time_now3 = Time::new(20, 0, 0, 0).unwrap();
assert!(is_inside_time_range(time_now, time_start, time_end)); assert!(is_inside_time_range(time_now, time_start, time_end));
assert!(!is_inside_time_range(time_now2, time_start, time_end)); assert!(!is_inside_time_range(time_now2, time_start, time_end));
@@ -503,4 +556,98 @@ mod tests {
assert!(actual.starts_with(&col_prefix)); assert!(actual.starts_with(&col_prefix));
assert!(actual.ends_with(&col_suffix)); assert!(actual.ends_with(&col_suffix));
} }
#[test]
fn config_check_invalid_tz() {
let actual = ModuleRenderer::new("time")
.config(toml::toml! {
[time]
disabled = false
time_format = "%T"
utc_time_offset = "invalid"
})
.collect();
assert!(actual.is_some(), "Falls back to local time");
}
#[test]
fn module_tz() {
use nu_ansi_term::Color;
let actual = ModuleRenderer::new("time")
.config(toml::toml! {
[time]
disabled = false
time_format = "%:z"
utc_time_offset = "Asia/Kolkata"
})
.collect();
let expected = Some(format!("at {} ", Color::Yellow.bold().paint("+05:30")));
assert_eq!(actual, expected, "Uses timezone");
}
#[test]
fn module_offset() {
use nu_ansi_term::Color;
let actual = ModuleRenderer::new("time")
.config(toml::toml! {
[time]
disabled = false
time_format = "%:z"
utc_time_offset = "-1.75"
})
.collect();
let expected = Some(format!("at {} ", Color::Yellow.bold().paint("-01:45")));
assert_eq!(actual, expected, "Uses timezone offset");
}
#[test]
fn module_tz_abbreviation() {
let actual = ModuleRenderer::new("time")
.config(toml::toml! {
[time]
disabled = false
time_format = "%Z"
utc_time_offset = "America/New_York"
})
.collect();
// Should output a timezone abbreviation like "EST" or "EDT"
assert!(
actual.is_some(),
"Timezone abbreviation should be displayed"
);
let output = actual.unwrap();
assert!(
output.contains("EST") || output.contains("EDT"),
"Should contain timezone abbreviation"
);
}
#[test]
fn module_tz_identifier() {
use nu_ansi_term::Color;
let actual = ModuleRenderer::new("time")
.config(toml::toml! {
[time]
disabled = false
time_format = "%Q"
utc_time_offset = "America/New_York"
})
.collect();
let expected = Some(format!(
"at {} ",
Color::Yellow.bold().paint("America/New_York")
));
assert_eq!(actual, expected, "Uses IANA timezone identifier");
}
} }
+30 -7
View File
@@ -527,7 +527,7 @@ impl ValueEnum for Preset {
} }
} }
pub fn preset_command(name: Option<Preset>, output: Option<PathBuf>, list: bool) { pub fn preset_command(name: Option<Preset>, output: Option<PathBuf>, force: bool, list: bool) {
if list { if list {
println!("{}", preset_list()); println!("{}", preset_list());
return; return;
@@ -535,11 +535,11 @@ pub fn preset_command(name: Option<Preset>, output: Option<PathBuf>, list: bool)
let variant = name.expect("name argument must be specified"); let variant = name.expect("name argument must be specified");
let content = shadow::get_preset_content(variant.0); let content = shadow::get_preset_content(variant.0);
if let Some(output) = output { if let Some(output) = output {
if let Err(err) = std::fs::write(output, content) { if let Err(e) = crate::utils::write_file_atomic(&output, content, force) {
eprintln!("Error writing preset to file: {err}"); eprintln!("Error writing preset to {output:?}: {e}");
std::process::exit(1); std::process::exit(1);
} }
} else if let Err(err) = std::io::stdout().write_all(content) { } else if let Err(err) = std::io::stdout().write_all(content.as_bytes()) {
eprintln!("Error writing preset to stdout: {err}"); eprintln!("Error writing preset to stdout: {err}");
std::process::exit(1); std::process::exit(1);
} }
@@ -681,9 +681,9 @@ mod test {
#[test] #[test]
fn preset_command_does_not_panic_on_correct_inputs() { fn preset_command_does_not_panic_on_correct_inputs() {
preset_command(None, None, true); preset_command(None, None, false, true);
for v in Preset::value_variants() { for v in Preset::value_variants() {
preset_command(Some(v.clone()), None, false); preset_command(Some(v.clone()), None, false, false);
} }
} }
@@ -691,12 +691,35 @@ mod test {
fn preset_command_output_to_file() -> std::io::Result<()> { fn preset_command_output_to_file() -> std::io::Result<()> {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("preset.toml"); let path = dir.path().join("preset.toml");
preset_command(Some(Preset("nerd-font-symbols")), Some(path.clone()), false); preset_command(
Some(Preset("nerd-font-symbols")),
Some(path.clone()),
false,
false,
);
let actual = utils::read_file(&path)?; let actual = utils::read_file(&path)?;
let expected = include_str!("../docs/public/presets/toml/nerd-font-symbols.toml"); let expected = include_str!("../docs/public/presets/toml/nerd-font-symbols.toml");
assert_eq!(actual, expected); assert_eq!(actual, expected);
dir.close()
}
#[test]
fn preset_command_output_existing_file_force() -> io::Result<()> {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("preset.toml");
utils::write_file(&path, "existing content")?;
preset_command(
Some(Preset("nerd-font-symbols")),
Some(path.clone()),
true,
false,
);
let actual = utils::read_file(&path).unwrap();
let expected = include_str!("../docs/public/presets/toml/nerd-font-symbols.toml");
assert_eq!(actual, expected);
dir.close() dir.close()
} }
+119 -5
View File
@@ -5,8 +5,8 @@ pub mod statusline;
use process_control::{ChildExt, Control}; use process_control::{ChildExt, Control};
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt::Debug; use std::fmt::Debug;
use std::fs::read_to_string; use std::fs;
use std::io::{Error, ErrorKind, Result}; use std::io::{Error, ErrorKind, Result, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -44,7 +44,7 @@ pub fn context_path<S: AsRef<OsStr> + ?Sized>(context: &Context, s: &S) -> PathB
pub fn read_file<P: AsRef<Path> + Debug>(file_name: P) -> Result<String> { pub fn read_file<P: AsRef<Path> + Debug>(file_name: P) -> Result<String> {
log::trace!("Trying to read from {file_name:?}"); log::trace!("Trying to read from {file_name:?}");
let result = read_to_string(file_name); let result = fs::read_to_string(file_name);
if result.is_err() { if result.is_err() {
log::debug!("Error reading file: {result:?}"); log::debug!("Error reading file: {result:?}");
@@ -58,8 +58,6 @@ pub fn read_file<P: AsRef<Path> + Debug>(file_name: P) -> Result<String> {
/// Write a string to a file /// Write a string to a file
#[cfg(test)] #[cfg(test)]
pub fn write_file<P: AsRef<Path>, S: AsRef<str>>(file_name: P, text: S) -> Result<()> { pub fn write_file<P: AsRef<Path>, S: AsRef<str>>(file_name: P, text: S) -> Result<()> {
use std::io::Write;
let file_name = file_name.as_ref(); let file_name = file_name.as_ref();
let text = text.as_ref(); let text = text.as_ref();
@@ -89,6 +87,73 @@ pub fn write_file<P: AsRef<Path>, S: AsRef<str>>(file_name: P, text: S) -> Resul
file.sync_all() file.sync_all()
} }
/// Write contents to a file by first writing to a temporary file
/// and then move it to the target location in place
/// Only overwrites existing files if `force` is true
pub fn write_file_atomic<P: AsRef<Path>, S: AsRef<str>>(
target_path: P,
text: S,
force: bool,
) -> std::result::Result<(), String> {
let target_path = target_path.as_ref();
let text = text.as_ref();
log::trace!("Trying to write {text:?} to {target_path:?}");
#[cfg_attr(not(unix), allow(unused_mut))]
let mut builder = tempfile::Builder::new();
// On Unix, the default permissions are too restrictive, so we need to relax them
// This should be safe because we're creating a temporary file in the same directory
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = target_path
.metadata()
.as_ref()
.map(fs::Metadata::permissions)
.unwrap_or_else(|_| {
let all_read_write = 0o666;
std::fs::Permissions::from_mode(all_read_write)
});
builder.permissions(permissions);
}
let Some(parent_dir) = target_path.parent() else {
return Err(format!(
"Unable to determine parent directory of {target_path:?}"
));
};
let mut temp_file = builder
.tempfile_in(parent_dir)
.map_err(|e| format!("Error creating temporary file: {}", e))?;
if let Err(err) = temp_file.write_all(text.as_bytes()) {
return Err(format!("Error writing to temporary file: {}", err));
}
let result = if force {
temp_file.persist(target_path)
} else {
temp_file.persist_noclobber(target_path)
};
result.map_err(|e| {
if !force && e.error.kind() == ErrorKind::AlreadyExists {
"Error saving file, use --force to overwrite existing configuration file".to_string()
} else {
format!("Error moving temporary file to target location: {e}")
}
})?;
log::trace!("File {target_path:?} written successfully");
Ok(())
}
/// Reads command output from stderr or stdout depending on to which stream program streamed it's output /// Reads command output from stderr or stdout depending on to which stream program streamed it's output
pub fn get_command_string_output(command: CommandOutput) -> String { pub fn get_command_string_output(command: CommandOutput) -> String {
if command.stdout.is_empty() { if command.stdout.is_empty() {
@@ -819,6 +884,8 @@ impl PathExt for Path {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use tempfile::tempdir;
use super::*; use super::*;
#[test] #[test]
@@ -1033,4 +1100,51 @@ mod tests {
"080d09bd815e".to_string() "080d09bd815e".to_string()
); );
} }
#[test]
fn test_write_file_atomic() -> Result<()> {
// Create a temporary file for testing
let tmp_dir = tempdir()?;
let path = tmp_dir.path().join("test_config.toml");
let expected = "test data";
write_file_atomic(&path, expected, false).unwrap();
let actual_data = read_file(&path)?;
assert_eq!(actual_data, expected);
tmp_dir.close()
}
#[test]
fn test_write_file_atomic_already_exists() -> Result<()> {
let tmp_dir = tempdir()?;
let tmp_file_path = tmp_dir.path().join("test_config.toml");
write_file(&tmp_file_path, "existing data")?;
let err = write_file_atomic(&tmp_file_path, "should not contain this", false).unwrap_err();
assert!(err.contains("--force"));
let actual_data = read_file(&tmp_file_path)?;
assert_eq!(actual_data, "existing data");
tmp_dir.close()
}
#[test]
fn test_write_file_atomic_overwrite() -> Result<()> {
let tmp_dir = tempdir()?;
let path = tmp_dir.path().join("test_config.toml");
write_file(&path, "existing data")?;
let expected = "test data";
write_file_atomic(&path, expected, true).unwrap();
let actual = read_file(&path)?;
assert_eq!(actual, expected);
tmp_dir.close()
}
} }
+101 -2
View File
@@ -1,6 +1,6 @@
use crate::module::ALL_MODULES; use crate::module::ALL_MODULES;
use serde::de::{ use serde::de::{
Deserializer, Error, IntoDeserializer, Visitor, self, Deserializer, Error, IntoDeserializer, Visitor,
value::{Error as ValueError, MapDeserializer, SeqDeserializer}, value::{Error as ValueError, MapDeserializer, SeqDeserializer},
}; };
use std::{cmp::Ordering, fmt}; use std::{cmp::Ordering, fmt};
@@ -100,6 +100,20 @@ impl ValueDeserializer<'_> {
_ => ValueError::custom(msg), _ => ValueError::custom(msg),
} }
} }
/// Return the fitting `de::Unexpected` type description for the given value.
/// For use with `Error::invalid_type`.
fn serde_unexpected(&self) -> de::Unexpected<'_> {
match self.value {
ValueRef::Boolean(b) => de::Unexpected::Bool(b),
ValueRef::Integer(i) => de::Unexpected::Signed(i),
ValueRef::Float(f) => de::Unexpected::Float(f),
ValueRef::String(s) => de::Unexpected::Str(s),
ValueRef::Array(_v) => de::Unexpected::Other("array"),
ValueRef::Table(_v) => de::Unexpected::Other("table"),
ValueRef::Datetime(_v) => de::Unexpected::Other("datetime"),
}
}
} }
impl<'de> IntoDeserializer<'de> for ValueDeserializer<'de> { impl<'de> IntoDeserializer<'de> for ValueDeserializer<'de> {
@@ -215,10 +229,29 @@ impl<'de> Deserializer<'de> for ValueDeserializer<'de> {
visitor.visit_newtype_struct(self) visitor.visit_newtype_struct(self)
} }
fn deserialize_enum<V>(
self,
_name: &'static str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match self.value {
// de::Value::StrDeserializer implements de::EnumAccess, so we can just use it.
ValueRef::String(s) => visitor.visit_enum(s.into_deserializer()),
_ => Err(Self::Error::invalid_type(
self.serde_unexpected(),
&"string",
)),
}
}
// Handle most deserialization cases by deferring to `deserialize_any`. // Handle most deserialization cases by deferring to `deserialize_any`.
serde::forward_to_deserialize_any! { serde::forward_to_deserialize_any! {
bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit seq bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit seq
bytes byte_buf map unit_struct tuple_struct enum tuple identifier bytes byte_buf map unit_struct tuple_struct tuple identifier
} }
} }
@@ -350,6 +383,72 @@ mod test {
assert_eq!(result.foo.0, "bar".to_owned()); assert_eq!(result.foo.0, "bar".to_owned());
} }
#[derive(Deserialize, PartialEq, Debug)]
#[serde(rename_all = "snake_case")]
enum SampleEnum {
FirstItem,
Second,
ThirdItem,
}
#[test]
fn test_deserialize_enum() {
#[derive(Deserialize)]
struct Sample {
foo: SampleEnum,
}
let value = toml::toml! {
foo = "first_item"
};
let deserializer = ValueDeserializer::new(&value);
let result = Sample::deserialize(deserializer).unwrap();
assert_eq!(result.foo, SampleEnum::FirstItem);
}
#[test]
fn test_deserialize_enum_unknown() {
#[derive(Deserialize)]
#[allow(dead_code)]
struct Sample {
foo: SampleEnum,
}
let value = toml::toml! {
foo = "unknown"
};
let deserializer = ValueDeserializer::new(&value);
let result = Sample::deserialize(deserializer);
assert!(result.is_err());
}
#[test]
fn test_deserialize_enum_invalid_type() {
#[derive(Deserialize, PartialEq, Debug)]
#[allow(dead_code)]
struct Sample {
foo: SampleEnum,
}
let value = toml::toml! {
foo = 1
};
let deserializer = ValueDeserializer::new(&value);
let result = Sample::deserialize(deserializer);
assert_eq!(
result,
Err(serde::de::Error::custom(
"invalid type: integer `1`, expected string"
))
);
}
#[test] #[test]
fn test_deserialize_unknown() { fn test_deserialize_unknown() {
let value = toml::toml! { let value = toml::toml! {