feedscope 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. feedscope-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +82 -0
  2. feedscope-0.1.0/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. feedscope-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +32 -0
  4. feedscope-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +21 -0
  5. feedscope-0.1.0/.github/dependabot.yml +22 -0
  6. feedscope-0.1.0/.github/workflows/ci.yml +50 -0
  7. feedscope-0.1.0/.github/workflows/release.yml +54 -0
  8. feedscope-0.1.0/.gitignore +13 -0
  9. feedscope-0.1.0/CHANGELOG.md +25 -0
  10. feedscope-0.1.0/CODE_OF_CONDUCT.md +79 -0
  11. feedscope-0.1.0/CONTRIBUTING.md +142 -0
  12. feedscope-0.1.0/LICENSE +21 -0
  13. feedscope-0.1.0/PKG-INFO +302 -0
  14. feedscope-0.1.0/README.md +271 -0
  15. feedscope-0.1.0/README.zh-CN.md +271 -0
  16. feedscope-0.1.0/SECURITY.md +39 -0
  17. feedscope-0.1.0/docs/release.md +66 -0
  18. feedscope-0.1.0/pyproject.toml +99 -0
  19. feedscope-0.1.0/scripts/demo_gif.py +143 -0
  20. feedscope-0.1.0/scripts/screenshot.py +828 -0
  21. feedscope-0.1.0/scripts/x_probe.py +62 -0
  22. feedscope-0.1.0/src/feedscope/__init__.py +3 -0
  23. feedscope-0.1.0/src/feedscope/__main__.py +4 -0
  24. feedscope-0.1.0/src/feedscope/cli.py +154 -0
  25. feedscope-0.1.0/src/feedscope/core/__init__.py +1 -0
  26. feedscope-0.1.0/src/feedscope/core/browser.py +367 -0
  27. feedscope-0.1.0/src/feedscope/core/config.py +367 -0
  28. feedscope-0.1.0/src/feedscope/core/const.py +64 -0
  29. feedscope-0.1.0/src/feedscope/core/display.py +62 -0
  30. feedscope-0.1.0/src/feedscope/core/http.py +104 -0
  31. feedscope-0.1.0/src/feedscope/core/i18n/__init__.py +202 -0
  32. feedscope-0.1.0/src/feedscope/core/i18n/en.toml +447 -0
  33. feedscope-0.1.0/src/feedscope/core/i18n/zh-CN.toml +444 -0
  34. feedscope-0.1.0/src/feedscope/core/media.py +150 -0
  35. feedscope-0.1.0/src/feedscope/core/models.py +86 -0
  36. feedscope-0.1.0/src/feedscope/core/registry.py +53 -0
  37. feedscope-0.1.0/src/feedscope/core/samples.py +68 -0
  38. feedscope-0.1.0/src/feedscope/core/source.py +668 -0
  39. feedscope-0.1.0/src/feedscope/core/text.py +54 -0
  40. feedscope-0.1.0/src/feedscope/core/timefmt.py +53 -0
  41. feedscope-0.1.0/src/feedscope/core/tree.py +134 -0
  42. feedscope-0.1.0/src/feedscope/doctor.py +54 -0
  43. feedscope-0.1.0/src/feedscope/login.py +168 -0
  44. feedscope-0.1.0/src/feedscope/probe.py +131 -0
  45. feedscope-0.1.0/src/feedscope/report.py +19 -0
  46. feedscope-0.1.0/src/feedscope/sources/__init__.py +38 -0
  47. feedscope-0.1.0/src/feedscope/sources/hn/__init__.py +66 -0
  48. feedscope-0.1.0/src/feedscope/sources/hn/fetch.py +82 -0
  49. feedscope-0.1.0/src/feedscope/sources/hn/parse.py +154 -0
  50. feedscope-0.1.0/src/feedscope/sources/reddit/__init__.py +178 -0
  51. feedscope-0.1.0/src/feedscope/sources/reddit/fetch.py +181 -0
  52. feedscope-0.1.0/src/feedscope/sources/reddit/parse.py +318 -0
  53. feedscope-0.1.0/src/feedscope/sources/rss/__init__.py +64 -0
  54. feedscope-0.1.0/src/feedscope/sources/rss/fetch.py +94 -0
  55. feedscope-0.1.0/src/feedscope/sources/rss/parse.py +116 -0
  56. feedscope-0.1.0/src/feedscope/sources/telegram/__init__.py +61 -0
  57. feedscope-0.1.0/src/feedscope/sources/telegram/fetch.py +79 -0
  58. feedscope-0.1.0/src/feedscope/sources/telegram/parse.py +182 -0
  59. feedscope-0.1.0/src/feedscope/sources/v2ex/__init__.py +64 -0
  60. feedscope-0.1.0/src/feedscope/sources/v2ex/fetch.py +46 -0
  61. feedscope-0.1.0/src/feedscope/sources/v2ex/parse.py +116 -0
  62. feedscope-0.1.0/src/feedscope/sources/x/__init__.py +68 -0
  63. feedscope-0.1.0/src/feedscope/sources/x/fetch.py +490 -0
  64. feedscope-0.1.0/src/feedscope/sources/x/parse.py +535 -0
  65. feedscope-0.1.0/src/feedscope/sources/zhihu/__init__.py +81 -0
  66. feedscope-0.1.0/src/feedscope/sources/zhihu/fetch.py +402 -0
  67. feedscope-0.1.0/src/feedscope/sources/zhihu/parse.py +235 -0
  68. feedscope-0.1.0/src/feedscope/subscriptions.py +423 -0
  69. feedscope-0.1.0/src/feedscope/ui/__init__.py +10 -0
  70. feedscope-0.1.0/src/feedscope/ui/account.py +125 -0
  71. feedscope-0.1.0/src/feedscope/ui/app.py +105 -0
  72. feedscope-0.1.0/src/feedscope/ui/dialogs.py +127 -0
  73. feedscope-0.1.0/src/feedscope/ui/feedback.py +64 -0
  74. feedscope-0.1.0/src/feedscope/ui/focus_nav.py +113 -0
  75. feedscope-0.1.0/src/feedscope/ui/labels.py +38 -0
  76. feedscope-0.1.0/src/feedscope/ui/layout.py +66 -0
  77. feedscope-0.1.0/src/feedscope/ui/list_render.py +42 -0
  78. feedscope-0.1.0/src/feedscope/ui/screens/__init__.py +1 -0
  79. feedscope-0.1.0/src/feedscope/ui/screens/answer.py +296 -0
  80. feedscope-0.1.0/src/feedscope/ui/screens/detail.py +274 -0
  81. feedscope-0.1.0/src/feedscope/ui/screens/language.py +59 -0
  82. feedscope-0.1.0/src/feedscope/ui/screens/main.py +736 -0
  83. feedscope-0.1.0/src/feedscope/ui/screens/main_setup.py +226 -0
  84. feedscope-0.1.0/src/feedscope/ui/screens/reading.py +384 -0
  85. feedscope-0.1.0/src/feedscope/ui/screens/source_setup.py +217 -0
  86. feedscope-0.1.0/src/feedscope/ui/screens/subscriptions.py +196 -0
  87. feedscope-0.1.0/src/feedscope/ui/theme.py +45 -0
  88. feedscope-0.1.0/src/feedscope/ui/wrap.py +157 -0
  89. feedscope-0.1.0/tests/conftest.py +172 -0
  90. feedscope-0.1.0/tests/test_account.py +305 -0
  91. feedscope-0.1.0/tests/test_architecture.py +171 -0
  92. feedscope-0.1.0/tests/test_browser.py +59 -0
  93. feedscope-0.1.0/tests/test_channel.py +218 -0
  94. feedscope-0.1.0/tests/test_config.py +298 -0
  95. feedscope-0.1.0/tests/test_display.py +30 -0
  96. feedscope-0.1.0/tests/test_doctor.py +56 -0
  97. feedscope-0.1.0/tests/test_guard.py +41 -0
  98. feedscope-0.1.0/tests/test_hn_parse.py +176 -0
  99. feedscope-0.1.0/tests/test_hn_source.py +169 -0
  100. feedscope-0.1.0/tests/test_http.py +93 -0
  101. feedscope-0.1.0/tests/test_i18n.py +145 -0
  102. feedscope-0.1.0/tests/test_i18n_no_hardcoded.py +119 -0
  103. feedscope-0.1.0/tests/test_language_screen.py +261 -0
  104. feedscope-0.1.0/tests/test_login.py +264 -0
  105. feedscope-0.1.0/tests/test_media.py +44 -0
  106. feedscope-0.1.0/tests/test_parsers.py +1118 -0
  107. feedscope-0.1.0/tests/test_probe.py +147 -0
  108. feedscope-0.1.0/tests/test_reddit_parse.py +186 -0
  109. feedscope-0.1.0/tests/test_reddit_source.py +499 -0
  110. feedscope-0.1.0/tests/test_rss_parse.py +149 -0
  111. feedscope-0.1.0/tests/test_rss_source.py +227 -0
  112. feedscope-0.1.0/tests/test_samples.py +58 -0
  113. feedscope-0.1.0/tests/test_scripts_smoke.py +23 -0
  114. feedscope-0.1.0/tests/test_source_caps.py +153 -0
  115. feedscope-0.1.0/tests/test_subs_screen.py +367 -0
  116. feedscope-0.1.0/tests/test_subscriptions.py +444 -0
  117. feedscope-0.1.0/tests/test_telegram_parse.py +133 -0
  118. feedscope-0.1.0/tests/test_telegram_source.py +244 -0
  119. feedscope-0.1.0/tests/test_text.py +62 -0
  120. feedscope-0.1.0/tests/test_theme.py +17 -0
  121. feedscope-0.1.0/tests/test_timefmt.py +22 -0
  122. feedscope-0.1.0/tests/test_tree.py +128 -0
  123. feedscope-0.1.0/tests/test_tui_smoke.py +1248 -0
  124. feedscope-0.1.0/tests/test_v2ex_parse.py +140 -0
  125. feedscope-0.1.0/tests/test_v2ex_source.py +124 -0
  126. feedscope-0.1.0/tests/test_wrap.py +68 -0
  127. feedscope-0.1.0/tests/test_x_fetch.py +319 -0
  128. feedscope-0.1.0/tests/test_zhihu_fetch.py +496 -0
  129. feedscope-0.1.0/uv.lock +853 -0
@@ -0,0 +1,82 @@
1
+ name: Bug report / 问题报告
2
+ description: Something is broken, renders wrong, or fails to fetch / 抓取失败、显示异常
3
+ labels: ["bug"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thanks for taking the time to report! 谢谢反馈!
9
+
10
+ Pasting `feedscope doctor` output up front (item 4) usually saves a
11
+ round trip — it carries the version, OS, Python and Chrome state in
12
+ four lines.
13
+ - type: textarea
14
+ id: what-happened
15
+ attributes:
16
+ label: What happened / 发生了什么
17
+ description: And what did you expect instead? 期望的行为是什么?
18
+ validations:
19
+ required: true
20
+ - type: dropdown
21
+ id: source
22
+ attributes:
23
+ label: Affected source / 受影响的源
24
+ multiple: true
25
+ options:
26
+ - Zhihu
27
+ - X (Twitter)
28
+ - Reddit
29
+ - Hacker News
30
+ - V2EX
31
+ - RSS
32
+ - Telegram
33
+ - TUI / app-wide (not source-specific)
34
+ - type: textarea
35
+ id: steps
36
+ attributes:
37
+ label: Steps to reproduce / 复现步骤
38
+ placeholder: |
39
+ 1. `feedscope`
40
+ 2. Tab to Reddit, wait for Popular
41
+ 3. ...
42
+ validations:
43
+ required: true
44
+ - type: textarea
45
+ id: doctor
46
+ attributes:
47
+ label: "`feedscope doctor` output / doctor 输出"
48
+ description: Run it in a terminal and paste the whole output — it includes version and environment in one shot.
49
+ placeholder: |
50
+ FeedScope doctor v0.1.0
51
+ OK Python 3.12
52
+ ...
53
+ render: text
54
+ validations:
55
+ required: true
56
+ - type: textarea
57
+ id: probe
58
+ attributes:
59
+ label: Probe sample (if a site redesign is suspected) / probe 样本(怀疑改版时)
60
+ description: |
61
+ `feedscope probe <source>` saves raw samples to disk — paste the
62
+ relevant excerpt if the error says a source may have changed. Mind
63
+ that samples can contain your feed content.
64
+ render: text
65
+ - type: dropdown
66
+ id: os
67
+ attributes:
68
+ label: OS / 操作系统
69
+ options:
70
+ - macOS
71
+ - Linux (Debian / Ubuntu)
72
+ - Linux (Arch)
73
+ - Linux (other)
74
+ - Windows (not supported yet — say more below)
75
+ - Other
76
+ validations:
77
+ required: true
78
+ - type: textarea
79
+ id: context
80
+ attributes:
81
+ label: Additional context / 其他补充
82
+ description: Logs, screenshots, config excerpts (minus anything sensitive) — anything that helps.
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Questions & discussions / 交流讨论
4
+ url: https://github.com/howardli/FeedScope/discussions
5
+ about: Usage questions, setups and ideas that aren't bugs or concrete features
6
+ - name: Security issues / 安全问题
7
+ url: https://github.com/howardli/FeedScope/security/policy
8
+ about: Please don't report vulnerabilities in public issues — see the security policy
@@ -0,0 +1,32 @@
1
+ name: Feature request / 功能建议
2
+ description: Suggest a new source, feed, or behavior / 新源、新流或行为建议
3
+ labels: ["enhancement"]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: What problem does this solve? / 它解决什么问题
9
+ description: Not the feature itself — the pain behind it. 不是功能本身,是它背后的痛点。
10
+ placeholder: I can't ... / 我没法……
11
+ validations:
12
+ required: true
13
+ - type: textarea
14
+ id: solution
15
+ attributes:
16
+ label: What would it look like? / 你期望的形态
17
+ description: Command, key, feed, source — sketch it however is natural.
18
+ validations:
19
+ required: true
20
+ - type: dropdown
21
+ id: area
22
+ attributes:
23
+ label: Area / 领域
24
+ options:
25
+ - New source (a new site / 新站点)
26
+ - New feed in an existing source
27
+ - Reading / detail view
28
+ - Login & accounts
29
+ - RSS / subscriptions
30
+ - Config & source manager
31
+ - i18n
32
+ - Other
@@ -0,0 +1,21 @@
1
+ ## Summary / 摘要
2
+
3
+ <!-- What changed and why. One or two sentences, then the detail that matters. -->
4
+
5
+ ## Type of change / 变更类型
6
+
7
+ - [ ] Bug fix
8
+ - [ ] New feature
9
+ - [ ] New source (registered alphabetically in `sources/__init__.py`; caps tests updated)
10
+ - [ ] Refactor / internals
11
+ - [ ] Docs / assets
12
+
13
+ ## Checklist / 自查
14
+
15
+ - [ ] Tests added or updated for the behavior change (fixture-driven, offline)
16
+ - [ ] `uv run ruff check src/ tests/ scripts/` clean
17
+ - [ ] `uv run mypy` clean
18
+ - [ ] `uv run pytest` green
19
+ - [ ] User-facing change has a `CHANGELOG.md` entry under `[Unreleased]`
20
+ - [ ] UI-visible change → screenshots / demo GIF regenerated (`scripts/screenshot.py`, `scripts/demo_gif.py`)
21
+ - [ ] New user-visible strings go through i18n catalogs (no hardcoded copy)
@@ -0,0 +1,22 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
7
+ commit-message:
8
+ prefix: "chore(deps)"
9
+ groups:
10
+ python-deps:
11
+ patterns:
12
+ - "*"
13
+ - package-ecosystem: github-actions
14
+ directory: "/"
15
+ schedule:
16
+ interval: weekly
17
+ commit-message:
18
+ prefix: "chore(ci)"
19
+ groups:
20
+ actions-deps:
21
+ patterns:
22
+ - "*"
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ - run: uv sync
15
+ - run: uv run ruff check src/ tests/ scripts/
16
+ - run: uv run mypy
17
+
18
+ test:
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ # classifiers 声明支持的平台都在矩阵里被真跑过(macOS 的 Chrome 路径 /
23
+ # platformdirs 目录差异以前只有本机验证);playwright 只装包不装浏览器,
24
+ # 测试基线全离线,hosted runner 直接可跑
25
+ os: [ubuntu-latest, macos-latest]
26
+ python-version: ["3.11", "3.12", "3.13"]
27
+ runs-on: ${{ matrix.os }}
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: astral-sh/setup-uv@v5
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ - run: uv sync --python ${{ matrix.python-version }}
34
+ - run: uv run pytest tests/ --cov --cov-fail-under=80
35
+ # 测试基线全离线(解析 fixture / TUI 冒烟 / 登录流程),hosted runner 直接可跑;
36
+ # feedscope probe 巡检需要真实登录态与 Chrome,只能挂 self-hosted runner 定时跑(见 README)
37
+ # coverage 门槛在 pyproject(fail_under=80,当前实测 ~89%):防倒退,新增模块应抬高它
38
+
39
+ build:
40
+ # 打包配置随时可构建:pyproject 的 packaging 回归在这里红,而不是发版当天才炸
41
+ runs-on: ubuntu-latest
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+ - uses: astral-sh/setup-uv@v5
45
+ - run: uv build
46
+ - uses: actions/upload-artifact@v4
47
+ with:
48
+ name: dist-preview
49
+ path: dist/
50
+ if-no-files-found: error
@@ -0,0 +1,54 @@
1
+ # Release pipeline: push a tag `v*` → build → publish to PyPI (OIDC trusted
2
+ # publishing, no tokens) → GitHub Release with the artifacts.
3
+ #
4
+ # One-time setup lives in docs/release.md (PyPI pending publisher + the
5
+ # `pypi` GitHub environment). After that, a release is: changelog entry,
6
+ # version bump, `git tag vX.Y.Z && git push --tags`.
7
+
8
+ name: Release
9
+
10
+ on:
11
+ push:
12
+ tags:
13
+ - "v*"
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: astral-sh/setup-uv@v5
21
+ - run: uv build # sdist + wheel; version is read from src/feedscope/__init__.py
22
+ - uses: actions/upload-artifact@v4
23
+ with:
24
+ name: dist
25
+ path: dist/
26
+ if-no-files-found: error
27
+
28
+ publish-pypi:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment: pypi # create this environment once; see docs/release.md
32
+ permissions:
33
+ id-token: write # OIDC trusted publishing — no PyPI token stored anywhere
34
+ steps:
35
+ - uses: actions/download-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+ - uses: pypa/gh-action-pypi-publish@release/v1
40
+
41
+ github-release:
42
+ needs: publish-pypi
43
+ runs-on: ubuntu-latest
44
+ permissions:
45
+ contents: write
46
+ steps:
47
+ - uses: actions/download-artifact@v4
48
+ with:
49
+ name: dist
50
+ path: dist/
51
+ - uses: softprops/action-gh-release@v2
52
+ with:
53
+ files: dist/*
54
+ generate_release_notes: true
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .ruff_cache/
5
+ .pytest_cache/
6
+ .claude/
7
+ dist/
8
+ .DS_Store
9
+ .zcode/
10
+ .screenshot-sources.toml
11
+ .coverage
12
+ coverage*.xml
13
+ cmp/
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-11-07
9
+
10
+ Initial release.
11
+
12
+ ### Added
13
+
14
+ - Multi-source aggregation in one terminal UI (`Tab` to switch): Zhihu Hot / Recommend / Following, X For You / Following, Reddit Home / Popular / custom subreddits, Hacker News Front / New / Ask / Show, V2EX Hot / Latest, Telegram channel subscriptions
15
+ - RSS subscriptions: any RSS / Atom feed; site-homepage feed auto-discovery; OPML import (idempotent merge) / export; "All" stream merged by publish time with cross-feed dedup; conditional requests (ETag / Last-Modified) keep refreshes polite to sources
16
+ - Three-layer in-terminal reading (feed → detail → discussion) loaded per layer on demand; HN-style deep threads flattened to two levels with `@parent` prefixes preserving who-replies-to-whom
17
+ - Login loop: in-app Chrome QR / password login with auto-refresh on completion, `Ctrl+U` to switch accounts; session-revocation detection that never leaves you guessing (X 401 semantics, Reddit `token_v2` JWT local decode — zero network, zero forgery)
18
+ - Resident browser-tab data channels for Zhihu / X / Reddit: TLS, header order and cookies fully consistent with the login session; X GraphQL queryIds self-heal by harvesting the current deployment's JS chunks; Reddit rides the website's own HTML channel with self-healing client version
19
+ - Auto Grok translation for foreign-language X posts; media rendered as clickable `[图片]` / `[视频]` markers linking to originals
20
+ - Bilingual UI (中文 / English), per-language TOML catalogs; a new language is one new TOML file, auto-discovered, enforced by key-parity and no-hardcoded-copy tests
21
+ - Source manager (`F2`): enable sources on demand, per-source feed `hide` config, subscription managers for RSS / Reddit / Telegram
22
+ - Editor-inspired VS Code Dark+ theme; every keybinding is a non-printing key (works while an IME is active); top tab bar and sidebar are clickable
23
+ - Layout-change self-check: 0 items parsed → immediate "site may have changed" error; `feedscope probe` inspects all sources with raw samples saved to disk; `feedscope doctor` environment self-check; `feedscope rss` CLI for subscription management
24
+
25
+ [0.1.0]: https://github.com/howardli/FeedScope/releases/tag/v0.1.0
@@ -0,0 +1,79 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the
26
+ overall community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or
31
+ advances of any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email
35
+ address, without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail
56
+ address, posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement — the maintainer
63
+ ([@howardli](https://github.com/howardli)) via a private message on GitHub.
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Attribution
70
+
71
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
72
+ version 2.1, available at
73
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
74
+
75
+ Impact assessments and some wording are drawn from the [Baseline Code of
76
+ Conduct](https://baseathon.github.io/baseline-code-of-conduct/).
77
+
78
+ [homepage]: https://www.contributor-covenant.org
79
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
@@ -0,0 +1,142 @@
1
+ # Contributing to FeedScope
2
+
3
+ Thanks for your interest in making FeedScope better. Bug reports, new sources,
4
+ new languages and doc improvements are all welcome — the two most natural
5
+ contributions for this codebase are **adding a source** and **adding a
6
+ language**, both designed to need almost no code outside their own files.
7
+
8
+ A note on language: the README is bilingual (English / 中文); code comments in
9
+ this repo are written in Chinese. Either language is fine in issues and PRs.
10
+
11
+ ## Getting started
12
+
13
+ Prerequisites: Python 3.11+, [uv](https://docs.astral.sh/uv/), and a local
14
+ Google Chrome (the app drives it; no `playwright install` needed).
15
+
16
+ ```bash
17
+ git clone https://github.com/howardli/FeedScope.git
18
+ cd FeedScope
19
+ uv sync # create the venv and install all dependencies
20
+ uv run feedscope # run the app from source
21
+ uv run pytest # run the full test suite — all offline, ~1 min
22
+ ```
23
+
24
+ The full quality gate (same as CI):
25
+
26
+ ```bash
27
+ uv run ruff check src/ tests/ scripts/
28
+ uv run mypy # src/ is typed and held at zero errors
29
+ uv run pytest tests/ --cov # coverage floor: 80%
30
+ ```
31
+
32
+ ## Project layout
33
+
34
+ ```
35
+ src/feedscope/
36
+ core/ models, config, registry, i18n, text/wrap/timefmt, tree,
37
+ browser channel — the foundation everything else builds on
38
+ sources/ one package per site: zhihu / x / reddit / hn / v2ex / rss / telegram
39
+ ui/ Textual screens and widgets
40
+ cli.py argparse entry point (tui / login / probe / doctor / rss)
41
+ tests/ offline suite: fixture-driven parsers, TUI smoke via Textual Pilot,
42
+ plus architecture / i18n / registry guard tests
43
+ scripts/ screenshot.py (README images), demo_gif.py, x_probe.py
44
+ ```
45
+
46
+ Layering is enforced by `tests/test_architecture.py`, not just convention:
47
+
48
+ ```
49
+ ui → top-level business (cli / login / subscriptions / probe / doctor / report)
50
+ → sources → core
51
+ ```
52
+
53
+ Dependencies only point downward; `parse.py` modules are pure leaves (no IO);
54
+ UI never reads source-private keys. If the guard blocks you, read its
55
+ docstring — the exceptions are documented there.
56
+
57
+ ## Adding a new source
58
+
59
+ The highest-value contribution. The architecture is plugin-shaped: a source is
60
+ one package under `src/feedscope/sources/`, registered by a single import —
61
+ UI and core need zero changes.
62
+
63
+ ```
64
+ sources/<id>/
65
+ fetch.py data layer: get raw data (httpx / resident browser tab /
66
+ DOM extraction); incremental loaders that hold resources live
67
+ here. No UI.
68
+ parse.py parsing layer: raw dict/HTML → FeedItem / Answer / DetailDoc /
69
+ markdown. Pure functions, directly testable against fixtures.
70
+ No network, no IO — it is the bottommost leaf.
71
+ __init__.py orchestration: the @register'd Source subclass — capability
72
+ declaration and open_* dispatch. First-screen guard via
73
+ require_feed_items; login gate via core.source.login_gate
74
+ if the site needs one.
75
+ ```
76
+
77
+ Step by step:
78
+
79
+ 1. **Read an existing source closest to your target shape.** RSS / Telegram
80
+ are subscription-style; HN / Reddit are three-layer with discussion trees;
81
+ V2EX is two-layer. `sources/__init__.py`'s docstring is the authoritative
82
+ template.
83
+ 2. **Write `parse.py` first** and drive it with saved fixtures
84
+ (`tests/test_<id>_parse.py`). This is where most of the logic should live,
85
+ because it is trivially testable offline.
86
+ 3. **Write `fetch.py`**: use `core/http.py` for plain HTTP, or the resident
87
+ browser-tab channel (`core/browser.py`) when the site blocks non-browser
88
+ fingerprints — see the Reddit / X sources for the pattern.
89
+ 4. **Write the `Source` subclass** in `__init__.py` with its capability flags
90
+ (`supports_detail`, `supports_comments`, `subscribable`, `feeds`,
91
+ `feed_labels`, `detail_kinds`). Inherit the shared skeletons instead of
92
+ rewriting mechanics:
93
+ - subscription-style → `core.source.SubscribedSource` (the "All" stream's
94
+ error handling comes free; inject `_fetch_merged` / `_open_one`)
95
+ - detail fetched as one full tree → `SliceDetailLoader` (floor / reply
96
+ paging = in-memory slicing, zero code)
97
+ - body carried by the list → `StaticDetailLoader` (opening a detail costs
98
+ zero requests)
99
+ - deep discussion flattening → `core.tree` (`@parent` prefix rule has a
100
+ single implementation)
101
+ 5. **Register it** by adding one import line to `sources/__init__.py`, in
102
+ **alphabetical order by `source_id`** — import order = source-picker row
103
+ order = default Tab order. `tests/test_source_caps.py` and
104
+ `tests/test_registry_order_is_tab_order` go red if you get this wrong.
105
+ 6. **All UI copy through i18n** (`core/i18n.t()`) — never hardcode user-visible
106
+ strings; `tests/test_i18n_no_hardcoded.py` blocks it in CI. Source
107
+ vocabulary words (unit labels like "回复" / "comment") go through the same
108
+ catalogs.
109
+ 7. **Run the full gate** (see above) and add a caps test entry if the source
110
+ introduces a new capability combination.
111
+
112
+ ## Adding a language
113
+
114
+ Copy `src/feedscope/core/i18n/en.toml` to `<code>.toml` (e.g. `ja.toml`), set
115
+ the native name in `[meta]`, translate the messages. The language picker
116
+ discovers it automatically — zero code changes. `tests/test_i18n.py` enforces
117
+ an identical key tree across catalogs, so a missing translation fails CI.
118
+
119
+ ## UI changes → refresh the images
120
+
121
+ README screenshots and the demo GIF are generated from the real app driven by
122
+ offline stub sources, so they can never drift from the code:
123
+
124
+ ```bash
125
+ uv run python scripts/screenshot.py # README stills → docs/*.png
126
+ uv run --with pillow python scripts/demo_gif.py # demo GIF → docs/demo.gif
127
+ ```
128
+
129
+ If your change affects what's on screen, re-run the relevant script and commit
130
+ the images together with the code.
131
+
132
+ ## Pull requests
133
+
134
+ - Keep PRs focused; one logical change each
135
+ - New behavior needs tests (offline; fixture-driven — look at existing
136
+ `tests/test_*_parse.py` for the pattern)
137
+ - User-facing changes get a `CHANGELOG.md` entry under `[Unreleased]`
138
+ - CI (ruff + mypy + pytest on Python 3.11–3.13, macOS & Linux) must be green
139
+ - Match the existing style: Chinese comments that explain *why*, ruff's
140
+ line-length 100 in `src/`
141
+
142
+ Thank you! 🎉
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 howardli
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.