9 Commits
Author SHA1 Message Date
nicknase27 ebf1be2cb9 add help menu and remove unnecessary function
Build / build (push) Successful in 2m11s
2026-07-27 18:51:11 +02:00
nicknase27 f721bdcd12 fix readme? 2026-07-27 15:34:00 +02:00
nicknase27 a854ac2e9d update readme 2026-07-27 15:32:53 +02:00
nicknase27 f7dafc843f add instructions to readme 2026-07-27 15:30:24 +02:00
nicknase27 1789e7b552 Cache Cargo dependencies
Build / build (push) Successful in 3m48s
2026-07-26 11:40:39 +02:00
nicknase27 ff6bc6ab4c workflow fix?
Build / build (push) Successful in 2m56s
2026-07-26 11:25:16 +02:00
nicknase27 68bc9f6a8a readme
Build / build (push) Failing after 15s
2026-07-26 11:23:08 +02:00
nicknase27 e4efa28647 Add release uploads to workflow 2026-07-26 11:21:06 +02:00
nicknase27 f681982fe5 add Readme?
Build / build (push) Canceled after 33s
2026-07-26 11:13:58 +02:00
4 changed files with 179 additions and 56 deletions
+18 -1
View File
@@ -18,6 +18,16 @@ jobs:
x86_64-unknown-linux-gnu x86_64-unknown-linux-gnu
x86_64-pc-windows-gnu x86_64-pc-windows-gnu
- uses: https://gitea.com/actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-${{ runner.os }}-
- name: Install dependencies - name: Install dependencies
run: | run: |
apt-get update apt-get update
@@ -37,5 +47,12 @@ jobs:
- name: Collect artifacts - name: Collect artifacts
run: | run: |
mkdir artifacts mkdir artifacts
cp target/x86_64-unknown-linux-gnu/release/ncmprs artifacts/ncmprs-linux cp target/x86_64-unknown-linux-gnu/release/ncmprs artifacts/ncmprs
cp target/x86_64-pc-windows-gnu/release/ncmprs.exe artifacts/ncmprs.exe cp target/x86_64-pc-windows-gnu/release/ncmprs.exe artifacts/ncmprs.exe
- name: Create Release
uses: akkuman/gitea-release-action@v1
with:
files: |
artifacts/ncmprs
artifacts/ncmprs.exe
+39
View File
@@ -0,0 +1,39 @@
# Setup
`ncmprs` requires a `config.toml` file that points to your FLAC music library.
## Linux
1. Create the configuration directory:
```sh
mkdir -p "$XDG_CONFIG_HOME/ncmprs"
```
2. Create the configuration file:
```sh
touch "$XDG_CONFIG_HOME/ncmprs/config.toml"
```
3. Add the path to your music library:
```toml
library_path = "/path/to/your/music"
```
---
## Windows
1. Create the configuration directory:
```
C:\Users\<YourUser>\AppData\Roaming\ncmprs\
```
2. Create a file named `config.toml` inside that directory.
3. Add the path to your music library:
```toml
library_path = "Z:\\Users\\<YourUser>\\Music"
```
> **Note:** Windows paths in TOML require backslashes to be escaped (`\\` instead of `\`).\
> **Note:** The first time you start `ncmprs`, it will take some time to index your library and create it's own database.
+47 -45
View File
@@ -42,6 +42,7 @@ pub struct App {
pub song_state: ListState, pub song_state: ListState,
pub queue_state: ListState, pub queue_state: ListState,
pub focus: Focus, pub focus: Focus,
pub control_popup: bool,
pub current_song: Option<usize>, pub current_song: Option<usize>,
pub paused_at: i64, pub paused_at: i64,
@@ -82,6 +83,7 @@ impl App {
song_state: ListState::default().with_selected(Some(0)), song_state: ListState::default().with_selected(Some(0)),
queue_state: ListState::default().with_selected(Some(0)), queue_state: ListState::default().with_selected(Some(0)),
focus: Focus::default(), focus: Focus::default(),
control_popup: false,
events: EventHandler::new(), events: EventHandler::new(),
current_song: None, current_song: None,
paused_at: 1, paused_at: 1,
@@ -131,7 +133,13 @@ impl App {
/// Handles the key events and updates the state of [`App`]. /// Handles the key events and updates the state of [`App`].
pub fn handle_key_events(&mut self, key_event: KeyEvent) -> color_eyre::Result<()> { pub fn handle_key_events(&mut self, key_event: KeyEvent) -> color_eyre::Result<()> {
match key_event.code { match key_event.code {
KeyCode::Esc | KeyCode::Char('q') => self.events.send(AppEvent::Quit), KeyCode::Esc | KeyCode::Char('q') => {
if self.control_popup == false {
self.events.send(AppEvent::Quit)
} else {
self.control_popup = !self.control_popup;
}
}
KeyCode::Char('c' | 'C') if key_event.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('c' | 'C') if key_event.modifiers == KeyModifiers::CONTROL => {
self.events.send(AppEvent::Quit) self.events.send(AppEvent::Quit)
} }
@@ -141,10 +149,16 @@ impl App {
self.library.save(); self.library.save();
} }
KeyCode::Char('?') => {
self.control_popup = !self.control_popup;
}
KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => { KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => {
self.events.send(AppEvent::MoveRight) self.events.send(AppEvent::MoveRight)
} }
KeyCode::Char('h') | KeyCode::Left => self.events.send(AppEvent::MoveLeft), KeyCode::Char('h') | KeyCode::Left | KeyCode::BackTab => {
self.events.send(AppEvent::MoveLeft)
}
KeyCode::Char('j') | KeyCode::Down => match self.focus { KeyCode::Char('j') | KeyCode::Down => match self.focus {
Focus::Artists => self.artist_state.select_next(), Focus::Artists => self.artist_state.select_next(),
@@ -212,49 +226,6 @@ impl App {
song_indexes.to_owned() song_indexes.to_owned()
} }
// pub fn play_pause(&mut self) {
// if self.current_song().is_some() {
// let song = &self.library.songs[self.current_song.unwrap()];
// let duration = song.duration.unwrap_or(1.0) as i64;
// let artist = &song.artist;
// if let Some(client) = self.discord_client.as_mut() {
// if !self.player.is_paused() {
// self.player.pause();
// self.paused_at = self.player.get_pos().as_secs() as i64;
//
// let asset = Assets::new();
// let activity = activity::Activity::new()
// .name("NCMPRS")
// .details(song.title.clone())
// .state("Paused")
// .activity_type(activity::ActivityType::Listening)
// .assets(asset.large_image("https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/navidrome.png"));
// let _ = client.set_activity(activity);
// } else if self.player.is_paused() {
// self.player.play();
// let now = SystemTime::now()
// .duration_since(UNIX_EPOCH)
// .unwrap()
// .as_secs() as i64;
// let timestamps = Timestamps::new()
// .start(now - self.paused_at)
// .end((now - self.paused_at) + duration);
// let asset = Assets::new();
//
// let payload = activity::Activity::new()
// .name("NCMPRS")
// .details(song.title.clone())
// .state(artist)
// .activity_type(activity::ActivityType::Listening)
// .timestamps(timestamps)
// .assets(asset.large_image("https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/navidrome.png"));
//
// let _ = client.set_activity(payload);
// }
// }
// }
// }
pub fn play_pause(&mut self) { pub fn play_pause(&mut self) {
let song_idx = match self.current_song { let song_idx = match self.current_song {
Some(idx) => idx, Some(idx) => idx,
@@ -413,6 +384,37 @@ impl App {
let _ = client.set_activity(activity); let _ = client.set_activity(activity);
} }
pub fn get_controls(&mut self) -> Vec<ratatui::widgets::Row<'_>> {
let rows = [
ratatui::widgets::Row::new(["", "General"]),
ratatui::widgets::Row::new(["", "Q / Esc / Ctrl+C", "Quit"]),
ratatui::widgets::Row::new(["", "R", "Rescan music library"]),
ratatui::widgets::Row::new(["", "?", "Show the help menu"]),
ratatui::widgets::Row::new(["", ""]),
ratatui::widgets::Row::new(["", "Navigation"]),
ratatui::widgets::Row::new(["", "H / Shift+Tab", "Left"]),
ratatui::widgets::Row::new(["", "J", "Down"]),
ratatui::widgets::Row::new(["", "K", "Up"]),
ratatui::widgets::Row::new(["", "L / Tab", "Right"]),
ratatui::widgets::Row::new(["", "g", "Jump to first item"]),
ratatui::widgets::Row::new(["", "G", "Jump to last item"]),
ratatui::widgets::Row::new(["", ""]),
ratatui::widgets::Row::new(["", "Playback"]),
ratatui::widgets::Row::new(["", "Enter", "Play selected song or album"]),
ratatui::widgets::Row::new(["", "Space", "Play / Pause"]),
ratatui::widgets::Row::new(["", "X", "Stop Playback"]),
ratatui::widgets::Row::new(["", ">", "Skip current track"]),
ratatui::widgets::Row::new(["", ""]),
ratatui::widgets::Row::new(["", "Queue"]),
ratatui::widgets::Row::new(["", "Delete", "Remove selected song from queue"]),
ratatui::widgets::Row::new(["", ""]),
ratatui::widgets::Row::new(["", "Volume"]),
ratatui::widgets::Row::new(["", "+ / =", "Increase volume"]),
ratatui::widgets::Row::new(["", "- / _", "Decrease volume"]),
];
return rows.to_vec();
}
/// Handles the tick event of the terminal. /// Handles the tick event of the terminal.
/// ///
/// The tick event is where you can update the state of your application with any logic that /// The tick event is where you can update the state of your application with any logic that
+75 -10
View File
@@ -1,10 +1,16 @@
use ratatui::{ use ratatui::{
buffer::Buffer, layout::{ buffer::Buffer,
layout::{
Alignment, Constraint, Alignment, Constraint,
Direction::{self}, Direction::{self},
Layout, Rect, Layout, Rect,
}, style::{Color, Modifier, Style}, symbols, widgets::{ },
Block, BorderType::{self, Rounded}, LineGauge, List, ListDirection, ListItem, Paragraph, StatefulWidget, Widget, style::{Color, Modifier, Style, Stylize},
symbols,
text::Line,
widgets::{
Block, BorderType::Rounded, Clear, LineGauge, List, ListDirection, ListItem, Paragraph,
Row, StatefulWidget, Table, Widget,
}, },
}; };
@@ -42,7 +48,12 @@ impl Widget for &mut App {
]) ])
.split(outer_layout[1]); .split(outer_layout[1]);
let now_playing_block = Block::bordered().title(format!("Now Playing | Vol: {:.0}", self.player.volume() * 100.0)).border_type(Rounded); let now_playing_block = Block::bordered()
.title(format!(
"Now Playing | Vol: {:.0}",
self.player.volume() * 100.0
))
.border_type(Rounded);
let inner = now_playing_block.inner(center_area[1]); let inner = now_playing_block.inner(center_area[1]);
Widget::render(now_playing_block, center_area[1], buf); Widget::render(now_playing_block, center_area[1], buf);
@@ -89,6 +100,7 @@ impl Widget for &mut App {
Color::Gray Color::Gray
}; };
// Artists
let artists: Vec<ListItem> = self let artists: Vec<ListItem> = self
.library .library
.artists .artists
@@ -100,7 +112,8 @@ impl Widget for &mut App {
.block( .block(
Block::bordered() Block::bordered()
.title("Artists") .title("Artists")
.border_style(Style::new().fg(artist_border_color)).border_type(Rounded), .border_style(Style::new().fg(artist_border_color))
.border_type(Rounded),
) )
.style(Style::new().white()) .style(Style::new().white())
.highlight_symbol(">>") .highlight_symbol(">>")
@@ -110,6 +123,13 @@ impl Widget for &mut App {
StatefulWidget::render(artist_list, left_area[0], buf, &mut self.artist_state); StatefulWidget::render(artist_list, left_area[0], buf, &mut self.artist_state);
// Info
let instructions = Line::from(vec![
" Help ".gray().bold(),
"<?> ".blue().bold(),
]);
// Albums
let albums: Vec<ListItem> = self let albums: Vec<ListItem> = self
.library .library
.albums_for_artist( .albums_for_artist(
@@ -123,7 +143,9 @@ impl Widget for &mut App {
.block( .block(
Block::bordered() Block::bordered()
.title("Albums") .title("Albums")
.border_style(Style::new().fg(album_border_color)).border_type(Rounded), .title_bottom(instructions.right_aligned())
.border_style(Style::new().fg(album_border_color))
.border_type(Rounded),
) )
.style(Style::new().white()) .style(Style::new().white())
.highlight_symbol(">>") .highlight_symbol(">>")
@@ -133,18 +155,26 @@ impl Widget for &mut App {
StatefulWidget::render(album_list, center_area[0], buf, &mut self.album_state); StatefulWidget::render(album_list, center_area[0], buf, &mut self.album_state);
// Songs
let song_indexes: Vec<usize> = self.current_selected_song(); let song_indexes: Vec<usize> = self.current_selected_song();
let song_items: Vec<ListItem> = song_indexes let song_items: Vec<ListItem> = song_indexes
.iter() .iter()
.map(|&index| ListItem::new(format!("{} ({})", self.library.songs[index].title.clone(), format_time(self.library.songs[index].duration.unwrap())))) .map(|&index| {
ListItem::new(format!(
"{} ({})",
self.library.songs[index].title.clone(),
format_time(self.library.songs[index].duration.unwrap())
))
})
.collect(); .collect();
let song_list = List::new(song_items) let song_list = List::new(song_items)
.block( .block(
Block::bordered() Block::bordered()
.title("Songs") .title("Songs")
.border_style(Style::new().fg(song_border_color)).border_type(Rounded), .border_style(Style::new().fg(song_border_color))
.border_type(Rounded),
) )
.style(Style::new().white()) .style(Style::new().white())
.highlight_symbol(">>") .highlight_symbol(">>")
@@ -154,14 +184,26 @@ impl Widget for &mut App {
StatefulWidget::render(song_list, right_area[0], buf, &mut self.song_state); StatefulWidget::render(song_list, right_area[0], buf, &mut self.song_state);
// Queue
let queue_items: Vec<ListItem> = self let queue_items: Vec<ListItem> = self
.queue .queue
.iter() .iter()
.map(|&index| ListItem::new(format!("{} ({}))", self.library.songs[index].title.as_str(), format_time(self.library.songs[index].duration.unwrap())))) .map(|&index| {
ListItem::new(format!(
"{} ({}))",
self.library.songs[index].title.as_str(),
format_time(self.library.songs[index].duration.unwrap())
))
})
.collect(); .collect();
let queue_list = List::new(queue_items) let queue_list = List::new(queue_items)
.block(Block::bordered().title("Queue").border_style(Style::new().fg(queue_border_color)).border_type(Rounded)) .block(
Block::bordered()
.title("Queue")
.border_style(Style::new().fg(queue_border_color))
.border_type(Rounded),
)
.style(Style::new().white()) .style(Style::new().white())
.highlight_symbol(">>") .highlight_symbol(">>")
.repeat_highlight_symbol(true) .repeat_highlight_symbol(true)
@@ -170,6 +212,29 @@ impl Widget for &mut App {
StatefulWidget::render(queue_list, right_area[1], buf, &mut self.queue_state); StatefulWidget::render(queue_list, right_area[1], buf, &mut self.queue_state);
// Help Menu
if self.control_popup {
let popup_block = Block::bordered().title("Controls").border_type(Rounded);
Widget::render(Clear, center_area[0], buf);
let header = Row::new(["", "Key", "Function"])
.style(Style::new().bold())
.bottom_margin(1);
let rows = self.get_controls();
let widths = [Constraint::Percentage(10), Constraint::Percentage(45), Constraint::Percentage(45)];
let help_menu = Table::new(rows, widths)
.header(header)
.column_spacing(1)
.style(Color::White)
.block(popup_block);
Widget::render(help_menu, center_area[0], buf);
}
// Play area
if let Some(song) = self.current_song() { if let Some(song) = self.current_song() {
let duration = song.duration.unwrap_or(1.0); let duration = song.duration.unwrap_or(1.0);
Paragraph::new(song.title.as_str()) Paragraph::new(song.title.as_str())