ltk/file_chooser/
places.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! The folders a file chooser offers to start from.

use std::path::{ Path, PathBuf };

use crate::files;

/// Where a browser's desktop mounts the folders of the user's computer, which a page has no home folder beside.
const MEDIA: &str = "/media";

#[ derive( Clone, Copy, Debug, PartialEq, Eq ) ]
pub enum PlaceKind
{
	Home,
	Desktop,
	Documents,
	Downloads,
	Music,
	Pictures,
	Videos,
	Root,
	/// A folder of the user's computer mounted in a browser.
	Mount,
}

#[ derive( Clone, Debug, PartialEq, Eq ) ]
pub struct Place
{
	pub kind: PlaceKind,
	pub path: PathBuf,
	/// A mount's own name; the others are named by their kind.
	pub name: Option<String>,
}

const USER_DIRS: [ ( PlaceKind, &str, &str ); 6 ] = [
	( PlaceKind::Desktop,   "DESKTOP",   "Desktop"   ),
	( PlaceKind::Documents, "DOCUMENTS", "Documents" ),
	( PlaceKind::Downloads, "DOWNLOAD",  "Downloads" ),
	( PlaceKind::Music,     "MUSIC",     "Music"     ),
	( PlaceKind::Pictures,  "PICTURES",  "Pictures"  ),
	( PlaceKind::Videos,    "VIDEOS",    "Videos"    ),
];

/// The user's home folder, or in a browser where its folders are mounted.
pub fn home() -> PathBuf
{
	crate::dirs::home().unwrap_or_else( || PathBuf::from( MEDIA ) )
}

/// The home folder, the user's folders that exist, the root, and the mounted folders.
pub fn standard() -> Vec<Place>
{
	let home    = home();
	let mut out = vec![ Place { kind: PlaceKind::Home, path: home.clone(), name: None } ];
	for ( kind, key, fallback ) in USER_DIRS
	{
		let path = user_dir( &home, key ).unwrap_or_else( || home.join( fallback ) );
		if path.is_dir() { out.push( Place { kind, path, name: None } ); }
	}
	out.extend( roots() );
	for mount in files::mounts().unwrap_or_default()
	{
		out.push( Place { kind: PlaceKind::Mount, path: mount.path, name: Some( mount.name ) } );
	}
	out
}

#[ cfg( not( windows ) ) ]
fn roots() -> Option<Place>
{
	Path::new( "/" ).is_dir().then( || Place { kind: PlaceKind::Root, path: PathBuf::from( "/" ), name: None } )
}

/// Each drive's root, named by its letter; from C, since asking an empty floppy drive may stall.
#[ cfg( windows ) ]
fn roots() -> Vec<Place>
{
	( 'C'..='Z' )
		.map( |letter| ( format!( "{letter}:" ), PathBuf::from( format!( "{letter}:\\" ) ) ) )
		.filter( |( _, path )| path.is_dir() )
		.map( |( name, path )| Place { kind: PlaceKind::Root, path, name: Some( name ) } )
		.collect()
}

/// From xdg-user-dirs; `None` when set to the home itself, which is how it disables one.
fn user_dir( home: &Path, key: &str ) -> Option<PathBuf>
{
	let config = crate::dirs::config_home().filter( |p| p.is_absolute() ).unwrap_or_else( || home.join( ".config" ) );
	let text   = std::fs::read_to_string( config.join( "user-dirs.dirs" ) ).ok()?;
	parse_user_dir( &text, home, key )
}

pub( crate ) fn parse_user_dir( text: &str, home: &Path, key: &str ) -> Option<PathBuf>
{
	let wanted = format!( "XDG_{key}_DIR" );
	let value  = text.lines()
		.filter( |l| !l.trim_start().starts_with( '#' ) )
		.filter_map( |l| l.split_once( '=' ) )
		.find( |( k, _ )| k.trim() == wanted )?
		.1.trim().trim_matches( '"' ).to_string();
	let path = match value.strip_prefix( "$HOME" )
	{
		Some( rest ) => home.join( rest.trim_start_matches( '/' ) ),
		None         => PathBuf::from( value ),
	};
	( path != home ).then_some( path )
}