Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Vim-style edge bell: scrolling past the top or bottom now emits a
terminal BEL. The visible effect (audible beep, title-bar 🔔, dock
bounce) is up to the terminal emulator — e.g. Ghostty surfaces a 🔔
in the window title via its default `bell-features`. Triggers on
`j`/`k`, `d`/`u`, `f`/`b`/`Space`/`PgUp`/`PgDn`; explicit jumps
(`gg`, `G`, `]`, `[`) stay silent. Disable with `--no-bell` or
`bell = false` in `~/.termdown/config.toml`.

### Changed
- Removed the 4-column outer margin that cat mode and TUI body rows
shared, plus the additional 2-column inset on cat-mode table rows.
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ termdown README.md | less
# Use a specific theme instead of auto-detect
termdown --theme light README.md

# Disable the edge-scroll bell (also configurable via `bell = false`)
termdown --no-bell README.md

# View help
termdown --help
termdown --version
Expand Down Expand Up @@ -143,6 +146,12 @@ termdown reads configuration from `~/.termdown/config.toml`.
# Auto-detection queries the terminal background color via OSC 11.
theme = "auto"

# Vim-style edge bell: emit a terminal BEL when you scroll past the
# top/bottom of the document. The terminal emulator decides the visible
# effect (audible beep, title-bar 🔔, dock bounce, …). Default true.
# CLI: `--no-bell`.
bell = true

[font.heading]
# English heading font (sans-serif recommended)
latin = "Inter"
Expand Down
7 changes: 7 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ termdown README.md | less
# 指定主题(不使用终端亮色、暗色主题自动检测)
termdown --theme light README.md

# 关闭文档到顶/到底时的提示铃声(也可在配置中设 `bell = false`)
termdown --no-bell README.md

# 查看帮助
termdown --help
termdown --version
Expand Down Expand Up @@ -135,6 +138,10 @@ TUI 模式需要指定文件路径,不支持从 stdin 读取。
# 自动检测通过 OSC 11 查询终端背景色。
theme = "auto"

# 文档到顶/到底时向终端发一次 BEL。具体表现(响铃、标题栏 🔔、
# dock 弹跳等)由终端模拟器决定。默认 true,命令行可用 `--no-bell` 关闭。
bell = true

[font.heading]
# 英文标题字体(推荐无衬线字体)
latin = "Inter"
Expand Down
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
- 在 `.github/workflows/ci.yml` 加一个 `msrv` job(`cargo check --all-targets` on pinned toolchain),防止以后 PR 悄悄抬高 MSRV
- [ ] 测试 markdown metadata 支持
- [ ] 检测文件变化
- [x] 文件到顶、末尾时,播放声音提示,增加喇叭icon
- [ ] t 开启目录时,支持左右等方向键在目录和内容之间切换,并可以有一些界面上的 focus 提示
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ pub struct Config {

/// Theme override: "dark", "light", or "auto" (default).
pub theme: Option<String>,

/// Vim-style edge bell: emit a terminal BEL when the user tries to scroll
/// past the top or bottom of the document. The terminal emulator decides
/// the visible effect (audible beep, title-bar 🔔, dock bounce, …) — see
/// e.g. Ghostty's `bell-features`. `None` means default (on). CLI
/// `--no-bell` overrides to `Some(false)`.
pub bell: Option<bool>,
}

#[derive(Deserialize, Default, Clone)]
Expand Down
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fn main() {
println!(" -V, --version Show version");
println!(" --theme <auto|dark|light> Color theme (default: auto-detect)");
println!(" --cat Force non-interactive cat-style output");
println!(" --no-bell Disable the edge-scroll terminal bell");
println!();
println!("By default, passing FILE opens it in the interactive TUI.");
println!("Piped/redirected stdout and stdin input automatically use cat mode.");
Expand All @@ -45,10 +46,14 @@ fn main() {
}

let cat_flag = args.iter().any(|a| a == "--cat");
let no_bell_flag = args.iter().any(|a| a == "--no-bell");

check_terminal_support();

let config = config::load();
let mut config = config::load();
if no_bell_flag {
config.bell = Some(false);
}

// Parse --theme flag (takes precedence over config).
let cli_theme = args
Expand Down
40 changes: 33 additions & 7 deletions src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,34 @@ fn event_loop<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Resu
}
}

/// Apply a scroll delta and ring the edge bell if the viewport didn't budge.
/// Detection lives here (not in `Viewport`) so the data layer stays free of
/// `App`/`Config`/audio coupling and `gg`/`G`/`]`/`[` — which bypass this
/// helper — silently stay non-belling, matching the chosen scope.
fn perform_scroll(app: &mut App, delta: i32) {
if delta == 0 {
return;
}
let before = app.active().viewport.top;
app.active_mut().viewport.scroll_by(delta);
if app.active().viewport.top == before {
ring_bell(&app.config);
}
}

/// Emit a terminal BEL on blocked edge-scroll. No-op when the user has
/// disabled bells via config or `--no-bell`. Writes to stderr (which is
/// unbuffered, so no manual flush) so the byte does not enter the
/// alternate-screen buffer. The visible "🔔 in the title bar" effect is the
/// terminal emulator's own response to BEL (e.g. Ghostty's `bell-features`
/// defaults include `title`), not something termdown paints.
fn ring_bell(config: &Config) {
if !config.bell.unwrap_or(true) {
return;
}
let _ = io::stderr().write_all(b"\x07");
}

fn handle_normal_key(app: &mut App, ev: &Event) -> io::Result<()> {
if let Event::Key(key) = ev {
if key.kind != event::KeyEventKind::Press {
Expand All @@ -381,16 +409,14 @@ fn handle_normal_key(app: &mut App, ev: &Event) -> io::Result<()> {
input::Action::Quit => {
app.should_quit = true;
}
input::Action::ScrollLines(d) => app.active_mut().viewport.scroll_by(d),
input::Action::ScrollLines(d) => perform_scroll(app, d),
input::Action::ScrollHalfPage(s) => {
let active = app.active_mut();
let delta = (active.viewport.height as i32 / 2) * s;
active.viewport.scroll_by(delta);
let delta = (app.active().viewport.height as i32 / 2) * s;
perform_scroll(app, delta);
}
input::Action::ScrollPage(s) => {
let active = app.active_mut();
let delta = active.viewport.height as i32 * s;
active.viewport.scroll_by(delta);
let delta = app.active().viewport.height as i32 * s;
perform_scroll(app, delta);
}
input::Action::JumpStart => app.active_mut().viewport.top = 0,
input::Action::JumpEnd => {
Expand Down
Loading