feat: init f9bc9c26
Steve · 2026-05-05 23:19 6 file(s) · +218 −0
.github/dependabot.yml (added) +17 −0
1 +
# To get started with Dependabot version updates, you'll need to specify which
2 +
# package ecosystems to update and where the package manifests are located.
3 +
# Please see the documentation for all configuration options:
4 +
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 +
6 +
version: 2
7 +
updates:
8 +
  # Maintain dependencies for Cargo
9 +
  - package-ecosystem: "cargo"
10 +
    directory: "/" # Location of package manifests
11 +
    schedule:
12 +
      interval: "weekly"
13 +
  # Maintain dependencies for GitHub Actions
14 +
  - package-ecosystem: github-actions
15 +
    directory: "/"
16 +
    schedule:
17 +
      interval: weekly
.github/workflows/ci.yml (added) +90 −0
1 +
name: CI
2 +
3 +
on:
4 +
  pull_request:
5 +
  push:
6 +
    branches:
7 +
      - main
8 +
      - master
9 +
      - develop
10 +
11 +
env:
12 +
  CARGO_TERM_COLOR: always
13 +
14 +
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
15 +
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
16 +
concurrency:
17 +
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
18 +
  cancel-in-progress: true
19 +
20 +
jobs:
21 +
  fmt:
22 +
    name: fmt
23 +
    runs-on: ubuntu-latest
24 +
    steps:
25 +
      - name: Checkout
26 +
        uses: actions/checkout@v4
27 +
      - name: Install Rust stable
28 +
        uses: dtolnay/rust-toolchain@stable
29 +
        with:
30 +
          components: rustfmt
31 +
      - name: check formatting
32 +
        run: cargo fmt -- --check
33 +
      - name: Cache Cargo dependencies
34 +
        uses: Swatinem/rust-cache@v2
35 +
  clippy:
36 +
    name: clippy
37 +
    runs-on: ubuntu-latest
38 +
    permissions:
39 +
      contents: read
40 +
      checks: write
41 +
    steps:
42 +
      - name: Checkout
43 +
        uses: actions/checkout@v4
44 +
      - name: Install Rust stable
45 +
        uses: dtolnay/rust-toolchain@stable
46 +
        with:
47 +
          components: clippy
48 +
      - name: Run clippy action
49 +
        uses: clechasseur/rs-clippy-check@v3
50 +
      - name: Cache Cargo dependencies
51 +
        uses: Swatinem/rust-cache@v2
52 +
  doc:
53 +
    # run docs generation on nightly rather than stable. This enables features like
54 +
    # https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an
55 +
    # API be documented as only available in some specific platforms.
56 +
    name: doc
57 +
    runs-on: ubuntu-latest
58 +
    steps:
59 +
      - uses: actions/checkout@v4
60 +
      - name: Install Rust nightly
61 +
        uses: dtolnay/rust-toolchain@nightly
62 +
      - name: Run cargo doc
63 +
        run: cargo doc --no-deps --all-features
64 +
        env:
65 +
          RUSTDOCFLAGS: --cfg docsrs
66 +
  test:
67 +
    runs-on: ${{ matrix.os }}
68 +
    name: test ${{ matrix.os }}
69 +
    strategy:
70 +
      fail-fast: false
71 +
      matrix:
72 +
        os: [macos-latest, windows-latest]
73 +
    steps:
74 +
      # if your project needs OpenSSL, uncomment this to fix Windows builds.
75 +
      # it's commented out by default as the install command takes 5-10m.
76 +
      # - run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
77 +
      #   if: runner.os == 'Windows'
78 +
      # - run: vcpkg install openssl:x64-windows-static-md
79 +
      #   if: runner.os == 'Windows'
80 +
      - uses: actions/checkout@v4
81 +
      - name: Install Rust
82 +
        uses: dtolnay/rust-toolchain@stable
83 +
      # enable this ci template to run regardless of whether the lockfile is checked in or not
84 +
      - name: cargo generate-lockfile
85 +
        if: hashFiles('Cargo.lock') == ''
86 +
        run: cargo generate-lockfile
87 +
      - name: cargo test --locked
88 +
        run: cargo test --locked --all-features --all-targets
89 +
      - name: Cache Cargo dependencies
90 +
        uses: Swatinem/rust-cache@v2
Cargo.toml (added) +20 −0
1 +
[package]
2 +
name = "bullet"
3 +
version = "0.1.0"
4 +
description = "Minimal RSS TUI"
5 +
authors = ["Steve <contact@stevedylan.dev>"]
6 +
license = "MIT"
7 +
edition = "2024"
8 +
9 +
[dependencies]
10 +
color-eyre = "0.6.3"
11 +
crossterm = "0.29.0"
12 +
feedparser-rs = "0.5.3"
13 +
ratatui = "0.30.0"
14 +
15 +
# Read the optimization guideline for more details: https://ratatui.rs/recipes/apps/release-your-app/#optimizations
16 +
[profile.release]
17 +
codegen-units = 1
18 +
lto = true
19 +
opt-level = "s"
20 +
strip = true
LICENSE (added) +21 −0
1 +
The MIT License (MIT)
2 +
3 +
Copyright (c) Steve <contact@stevedylan.dev>
4 +
5 +
Permission is hereby granted, free of charge, to any person obtaining a copy
6 +
of this software and associated documentation files (the "Software"), to deal
7 +
in the Software without restriction, including without limitation the rights
8 +
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 +
copies of the Software, and to permit persons to whom the Software is
10 +
furnished to do so, subject to the following conditions:
11 +
12 +
The above copyright notice and this permission notice shall be included in all
13 +
copies or substantial portions of the Software.
14 +
15 +
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 +
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 +
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 +
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 +
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 +
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 +
SOFTWARE.
README.md (added) +14 −0
1 +
# bullet
2 +
3 +
This is a [Ratatui] app generated by the [Hello World template].
4 +
5 +
[Ratatui]: https://ratatui.rs
6 +
[Hello World Template]: https://github.com/ratatui/templates/tree/main/hello-world
7 +
8 +
## License
9 +
10 +
Copyright (c) Steve <contact@stevedylan.dev>
11 +
12 +
This project is licensed under the MIT license ([LICENSE] or <http://opensource.org/licenses/MIT>)
13 +
14 +
[LICENSE]: ./LICENSE
src/main.rs (added) +56 −0
1 +
use feedparser_rs::{ParsedFeed, parse_url};
2 +
use ratatui::{
3 +
    DefaultTerminal, Frame,
4 +
    style::{Color, Modifier, Style},
5 +
    text::{Line, Span, Text},
6 +
    widgets::List,
7 +
    widgets::ListItem,
8 +
};
9 +
10 +
fn main() -> color_eyre::Result<()> {
11 +
    color_eyre::install()?;
12 +
    let feed = parse_url("https://feeds.stevedylan.dev/feed.xml", None, None, None)?;
13 +
    ratatui::run(|t| app(t, &feed))?;
14 +
    Ok(())
15 +
}
16 +
17 +
fn app(terminal: &mut DefaultTerminal, feed: &ParsedFeed) -> std::io::Result<()> {
18 +
    loop {
19 +
        terminal.draw(|f| render(f, feed))?;
20 +
        if crossterm::event::read()?.is_key_press() {
21 +
            break Ok(());
22 +
        }
23 +
    }
24 +
}
25 +
26 +
fn render(frame: &mut Frame, feed: &ParsedFeed) {
27 +
    let dim = Style::new().fg(Color::DarkGray);
28 +
    let author_style = Style::new()
29 +
        .fg(Color::DarkGray)
30 +
        .add_modifier(Modifier::ITALIC);
31 +
32 +
    let items: Vec<ListItem> = feed
33 +
        .entries
34 +
        .iter()
35 +
        .map(|e| {
36 +
            let date = e
37 +
                .published
38 +
                .as_ref()
39 +
                .map(|d| d.to_string())
40 +
                .unwrap_or_else(|| "-".into());
41 +
            let title = e.title.as_deref().unwrap_or("(untitled)");
42 +
            let author = e
43 +
                .authors
44 +
                .first()
45 +
                .and_then(|a| a.name.as_deref())
46 +
                .unwrap_or("anon");
47 +
            ListItem::new(Text::from(vec![
48 +
                Line::from(Span::styled(date, dim)),
49 +
                Line::from(title.to_string()),
50 +
                Line::from(Span::styled(author.to_string(), author_style)),
51 +
                Line::from(""),
52 +
            ]))
53 +
        })
54 +
        .collect();
55 +
    frame.render_widget(List::new(items), frame.area());
56 +
}