added config

This commit is contained in:
2026-02-13 22:03:18 -06:00
parent 8b562a2780
commit 5f07f6e611
5 changed files with 185 additions and 2 deletions

103
src/core/config.rs Normal file
View File

@@ -0,0 +1,103 @@
use std::{fs, io, path::PathBuf};
use super::paths;
#[derive(Debug, Clone)]
pub struct Config {
pub base_url: String,
pub access_token: Option<String>,
pub user_id: Option<String>,
}
#[derive(Debug)]
pub enum ConfigError {
NotFound(PathBuf),
Io(io::Error),
Parse(String),
}
impl From<io::Error> for ConfigError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
pub fn config_path() -> Option<PathBuf> {
paths::app_config_dir().map(|d| d.join("config.toml"))
}
// Very small TOML-ish parser for now (no deps).
// Format:
// base_url = "http://..."
// access_token = "..."
pub fn load_config() -> Result<Config, ConfigError> {
let path = config_path().ok_or_else(|| ConfigError::Parse("No HOME/XDG dirs".into()))?;
if !path.exists() {
return Err(ConfigError::NotFound(path));
}
let s = fs::read_to_string(&path)?;
parse_config(&s).map_err(ConfigError::Parse)
}
fn parse_config(s: &str) -> Result<Config, String> {
let mut base_url: Option<String> = None;
let mut access_token: Option<String> = None;
let mut user_id: Option<String> = None;
for (i, line) in s.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (k, v) = line
.split_once('=')
.ok_or_else(|| format!("Line {}: expected key = value", i + 1))?;
let key = k.trim();
let mut val = v.trim().to_string();
// strip quotes if present
if val.starts_with('"') && val.ends_with('"') && val.len() >= 2 {
val = val[1..val.len() - 1].to_string();
}
match key {
"base_url" => base_url = Some(val),
"access_token" => access_token = Some(val),
"user_id" => user_id = Some(val),
_ => {}
}
}
let base_url = base_url.ok_or_else(|| "Missing base_url".to_string())?;
Ok(Config {
base_url,
access_token,
user_id,
})
}
pub fn save_config(cfg: &Config) -> Result<(), ConfigError> {
let dir = paths::app_config_dir()
.ok_or_else(|| ConfigError::Parse("No HOME/XDG dirs".into()))?;
std::fs::create_dir_all(&dir)?;
let path = dir.join("config.toml");
let mut out = String::new();
out.push_str(&format!("base_url = \"{}\"\n", cfg.base_url));
if let Some(t) = &cfg.access_token {
out.push_str(&format!("access_token = \"{}\"\n", t));
}
if let Some(u) = &cfg.user_id {
out.push_str(&format!("user_id = \"{}\"\n", u));
}
std::fs::write(path, out)?;
Ok(())
}

33
src/core/jellyfin.rs Normal file
View File

@@ -0,0 +1,33 @@
use super::config::Config;
#[derive(Clone)]
pub struct JellyfinClient {
pub cfg: Config,
}
#[derive(Debug, Clone)]
pub struct JfItem {
pub id: String,
pub name: String,
pub item_type: String, // "Movie", "Series", etc.
pub image_tag: Option<String>,
}
impl JellyfinClient {
pub fn new(cfg: Config) -> Self {
Self { cfg }
}
// Placeholder for now — next step will use reqwest and actually call Jellyfin.
pub fn test_connection_stub(&self) -> Result<String, String> {
Ok(format!("Configured for {}", self.cfg.base_url))
}
// Placeholder list so we can wire UI without network yet
pub fn list_movies_stub(&self) -> Vec<JfItem> {
vec![
JfItem { id: "1".into(), name: "Movie A".into(), item_type: "Movie".into(), image_tag: None },
JfItem { id: "2".into(), name: "Movie B".into(), item_type: "Movie".into(), image_tag: None },
]
}
}

3
src/core/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod config;
pub mod jellyfin;
pub mod paths;

35
src/core/paths.rs Normal file
View File

@@ -0,0 +1,35 @@
use std::{env, path::PathBuf};
pub fn home_dir() -> Option<PathBuf> {
env::var_os("HOME").map(PathBuf::from)
}
pub fn config_dir() -> Option<PathBuf> {
// XDG first, fallback to ~/.config
if let Some(p) = env::var_os("XDG_CONFIG_HOME").map(PathBuf::from) {
Some(p)
} else {
home_dir().map(|h| h.join(".config"))
}
}
pub fn cache_dir() -> Option<PathBuf> {
// XDG first, fallback to ~/.cache
if let Some(p) = env::var_os("XDG_CACHE_HOME").map(PathBuf::from) {
Some(p)
} else {
home_dir().map(|h| h.join(".cache"))
}
}
pub fn app_config_dir() -> Option<PathBuf> {
config_dir().map(|d| d.join("cj-jftui"))
}
pub fn app_cache_dir() -> Option<PathBuf> {
cache_dir().map(|d| d.join("cj-jftui"))
}
pub fn posters_cache_dir() -> Option<PathBuf> {
app_cache_dir().map(|d| d.join("posters"))
}

View File

@@ -1,3 +1,5 @@
mod core;
use std::{ use std::{
io, io,
time::{Duration, Instant}, time::{Duration, Instant},
@@ -26,6 +28,7 @@ enum View {
struct App { struct App {
should_exit: bool, should_exit: bool,
status: String, status: String,
config_status: String,
view_stack: Vec<View>, view_stack: Vec<View>,
@@ -35,11 +38,16 @@ struct App {
impl Default for App { impl Default for App {
fn default() -> Self { fn default() -> Self {
let config_status = match core::config::load_config() {
Ok(cfg) => format!("config: ok ({})", cfg.base_url),
Err(e) => format!("config: missing/invalid ({:?})", e),
};
Self { Self {
should_exit: false, should_exit: false,
status: "Press q to quit".to_string(), status: "Press q to quit".to_string(),
config_status,
view_stack: vec![View::Categories], view_stack: vec![View::Categories],
items: vec![ items: vec![
"Shows".into(), "Shows".into(),
"Movies".into(), "Movies".into(),
@@ -178,7 +186,8 @@ fn ui(frame: &mut Frame, app: &App) {
panes.left_output, panes.left_output,
); );
frame.render_widget( frame.render_widget(
Block::bordered().title("Info").merge_borders(MergeStrategy::Exact), Paragraph::new(app.config_status.as_str())
.block(Block::bordered().title("Info").merge_borders(MergeStrategy::Exact)),
panes.right_output, panes.right_output,
); );
} }