mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
hopper stuffs
This commit is contained in:
5
assets/disk0/home/config/zfmrc
Normal file
5
assets/disk0/home/config/zfmrc
Normal file
@@ -0,0 +1,5 @@
|
||||
[EXEC_FUNS]
|
||||
nes,A:/home/tvnes/tvnes.js {0}
|
||||
|
||||
[COL_HL_EXT]
|
||||
nes,156
|
||||
1
assets/disk0/hopper/bin/hop.alias
Normal file
1
assets/disk0/hopper/bin/hop.alias
Normal file
@@ -0,0 +1 @@
|
||||
hopper $0
|
||||
12
assets/disk0/hopper/bin/hop.per
Normal file
12
assets/disk0/hopper/bin/hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:hopper
|
||||
HopperPackageVersion:0.0.1
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:hopper;
|
||||
HopperRequires:tvdos 1.*
|
||||
ProperName:Hopper
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:Package manager for TVDOS
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
PackageFileList:https://raw.githubusercontent.com/curioustorvald/hopper/refs/heads/master/hopper.js;
|
||||
@@ -1,6 +1,303 @@
|
||||
/**
|
||||
* Hopper is a package manager for TSVM
|
||||
* Hopper is a package manager for TVDOS
|
||||
* Created by CuriousTorvald on 2026-04-16
|
||||
*/
|
||||
|
||||
println("Hopper - Package manager for TSVM")
|
||||
const SYSTEM_PACKEAGE_DEF_DIR = "A:/tvdos/hopper"
|
||||
const MANIFEST_EXT = "hop.per"
|
||||
|
||||
// SYNOPSIS
|
||||
// hopper {search,se} [--provides, --requires, --description, --author] query
|
||||
//// default searches from ProperName
|
||||
// hopper {install,in} query [-v version]
|
||||
// hopper {remove,rm} query
|
||||
|
||||
// ============================================================
|
||||
// Manifest parsing
|
||||
// ============================================================
|
||||
|
||||
function splitList(s) {
|
||||
if (!s) return []
|
||||
return s.split(";").map(it => it.trim()).filter(it => it.length > 0)
|
||||
}
|
||||
|
||||
function parseManifest(text) {
|
||||
const m = {}
|
||||
text.split("\n").forEach(rawLine => {
|
||||
const line = rawLine.replace(/\r$/, "")
|
||||
if (line.length === 0) return
|
||||
const idx = line.indexOf(":")
|
||||
if (idx < 0) return
|
||||
const key = line.substring(0, idx).trim()
|
||||
const value = line.substring(idx + 1).trim()
|
||||
m[key] = value
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
function readManifestFile(path) {
|
||||
const f = files.open(path)
|
||||
if (!f.exists || f.isDirectory) return undefined
|
||||
const m = parseManifest(f.sread())
|
||||
m._manifestPath = path
|
||||
return m
|
||||
}
|
||||
|
||||
function listInstalledManifests() {
|
||||
const dir = files.open(SYSTEM_PACKEAGE_DEF_DIR)
|
||||
if (!dir.exists || !dir.isDirectory) return []
|
||||
const out = []
|
||||
dir.list().forEach(entry => {
|
||||
if (entry.isDirectory) return
|
||||
if (!entry.name.toLowerCase().endsWith(MANIFEST_EXT)) return
|
||||
const m = readManifestFile(entry.fullPath)
|
||||
if (m !== undefined) out.push(m)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
function findInstalledManifest(name) {
|
||||
const direct = `${SYSTEM_PACKEAGE_DEF_DIR}/${name}${MANIFEST_EXT}`
|
||||
const m = readManifestFile(direct)
|
||||
if (m !== undefined) return m
|
||||
const all = listInstalledManifests()
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
if ((all[i].HopperPackageName || "") === name) return all[i]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Search
|
||||
// ============================================================
|
||||
|
||||
// Dummy "remote" repository -- pretends to be a network query result.
|
||||
const FAKE_REMOTE_PACKAGES = [
|
||||
{
|
||||
HopperPackageName: "doomster",
|
||||
HopperPackageVersion: "0.9.3",
|
||||
ProperName: "Doomster",
|
||||
ProperAuthor: "id Sortware",
|
||||
ProperDescription: "First-person shooter game for TSVM",
|
||||
HopperProvides: "doomster;",
|
||||
HopperRequires: "tvdos 1.*;libgl 1.*"
|
||||
},
|
||||
{
|
||||
HopperPackageName: "libfft",
|
||||
HopperPackageVersion: "0.1.0",
|
||||
ProperName: "LibFFT",
|
||||
ProperAuthor: "Soraya Vaughn",
|
||||
ProperDescription: "Fast Fourier Transform library for TSVM",
|
||||
HopperProvides: "libfft;",
|
||||
HopperRequires: "tvdos 1.*"
|
||||
},
|
||||
{
|
||||
HopperPackageName: "chatlite",
|
||||
HopperPackageVersion: "2.1.5",
|
||||
ProperName: "ChatLite",
|
||||
ProperAuthor: "TerraNetworks Co.",
|
||||
ProperDescription: "Lightweight IRC-style chat client",
|
||||
HopperProvides: "chatlite;",
|
||||
HopperRequires: "tvdos 1.*;wintex 1.*"
|
||||
},
|
||||
{
|
||||
HopperPackageName: "snakey",
|
||||
HopperPackageVersion: "1.4.0",
|
||||
ProperName: "Snakey",
|
||||
ProperAuthor: "Iben Holst",
|
||||
ProperDescription: "Classic snake game with TerranBASIC scripting",
|
||||
HopperProvides: "snakey;",
|
||||
HopperRequires: "tvdos 1.*;libterranbasic 1.*"
|
||||
}
|
||||
]
|
||||
|
||||
function fieldCandidates(manifest, field) {
|
||||
switch (field) {
|
||||
case "provides": return splitList(manifest.HopperProvides || "")
|
||||
case "requires": return splitList(manifest.HopperRequires || "")
|
||||
case "description": return [manifest.ProperDescription || ""]
|
||||
case "author": return [manifest.ProperAuthor || ""]
|
||||
default: return [manifest.ProperName || "", manifest.HopperPackageName || ""]
|
||||
}
|
||||
}
|
||||
|
||||
function matchesQuery(manifest, field, query) {
|
||||
const q = query.toLowerCase()
|
||||
return fieldCandidates(manifest, field).some(c => c.toLowerCase().indexOf(q) >= 0)
|
||||
}
|
||||
|
||||
function printSearchResult(m, origin) {
|
||||
const name = m.ProperName || m.HopperPackageName || "(unnamed)"
|
||||
const ver = m.HopperPackageVersion || "?"
|
||||
println(` [${origin}] ${name} -- ${m.HopperPackageName} ${ver}`)
|
||||
if (m.ProperDescription) println(` ${m.ProperDescription}`)
|
||||
}
|
||||
|
||||
function cmdSearch(args) {
|
||||
let field = "name"
|
||||
let query = undefined
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i]
|
||||
if (a === "--provides") field = "provides"
|
||||
else if (a === "--requires") field = "requires"
|
||||
else if (a === "--description") field = "description"
|
||||
else if (a === "--author") field = "author"
|
||||
else if (a.startsWith("--")) { printerrln(`Unknown option: ${a}`); return 1 }
|
||||
else query = a
|
||||
}
|
||||
if (query === undefined) {
|
||||
printerrln("Usage: hopper search [--provides|--requires|--description|--author] <query>")
|
||||
return 1
|
||||
}
|
||||
|
||||
println(`Searching installed packages in ${SYSTEM_PACKEAGE_DEF_DIR} ...`)
|
||||
const sysHits = listInstalledManifests().filter(m => matchesQuery(m, field, query))
|
||||
if (sysHits.length === 0) println(" (no matches)")
|
||||
else sysHits.forEach(m => printSearchResult(m, "installed"))
|
||||
|
||||
println("")
|
||||
println("Searching remote repository ...")
|
||||
const netHits = FAKE_REMOTE_PACKAGES.filter(m => matchesQuery(m, field, query))
|
||||
if (netHits.length === 0) println(" (no matches)")
|
||||
else netHits.forEach(m => printSearchResult(m, "remote"))
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Install (pure dummy)
|
||||
// ============================================================
|
||||
|
||||
function cmdInstall(args) {
|
||||
let query = undefined
|
||||
let version = undefined
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === "-v") { version = args[i + 1]; i++ }
|
||||
else if (args[i].startsWith("--")) { printerrln(`Unknown option: ${args[i]}`); return 1 }
|
||||
else query = args[i]
|
||||
}
|
||||
if (query === undefined) {
|
||||
printerrln("Usage: hopper install <package> [-v <version>]")
|
||||
return 1
|
||||
}
|
||||
|
||||
const verSuffix = version ? ` (v${version})` : ""
|
||||
println(`Resolving ${query}${verSuffix} ...`)
|
||||
println(`Fetching manifest from remote ...`)
|
||||
println(`Resolving dependencies ...`)
|
||||
println(`Downloading package payload ...`)
|
||||
println(`Verifying integrity ...`)
|
||||
println(`Writing manifest to ${SYSTEM_PACKEAGE_DEF_DIR}/${query}${MANIFEST_EXT} ...`)
|
||||
println(`Installed ${query}${verSuffix}.`)
|
||||
println("(dummy install: no files were actually created)")
|
||||
return 0
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Remove (dry-run; resolves file list from manifest)
|
||||
// ============================================================
|
||||
|
||||
// Convert a SystemPackagePath entry (e.g. "/tvdos/bin/taut*") into a
|
||||
// concrete list of files on the A: drive. Supports a simple '*' wildcard
|
||||
// in the filename component.
|
||||
function expandSystemPath(pattern) {
|
||||
const sysDrive = "A:"
|
||||
|
||||
if (pattern.indexOf("*") < 0) {
|
||||
return [`${sysDrive}${pattern}`]
|
||||
}
|
||||
|
||||
const fwd = pattern.lastIndexOf("/")
|
||||
const bck = pattern.lastIndexOf("\\")
|
||||
const lastSep = Math.max(fwd, bck)
|
||||
const dirPart = (lastSep < 0) ? "" : pattern.substring(0, lastSep)
|
||||
const namePart = (lastSep < 0) ? pattern : pattern.substring(lastSep + 1)
|
||||
|
||||
const dir = files.open(`${sysDrive}${dirPart}/`)
|
||||
if (!dir.exists || !dir.isDirectory) return []
|
||||
|
||||
const escaped = namePart.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")
|
||||
const re = new RegExp(`^${escaped}$`, "i")
|
||||
|
||||
const out = []
|
||||
dir.list().forEach(entry => {
|
||||
if (entry.isDirectory) return
|
||||
if (re.test(entry.name)) out.push(entry.fullPath)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
function cmdRemove(args) {
|
||||
const query = args[0]
|
||||
if (query === undefined) {
|
||||
printerrln("Usage: hopper remove <package>")
|
||||
return 1
|
||||
}
|
||||
|
||||
const m = findInstalledManifest(query)
|
||||
if (m === undefined) {
|
||||
printerrln(`Package not installed: ${query}`)
|
||||
return 2
|
||||
}
|
||||
|
||||
const name = m.ProperName || m.HopperPackageName || query
|
||||
const ver = m.HopperPackageVersion || "?"
|
||||
println(`Preparing removal of ${name} (${m.HopperPackageName} ${ver}) ...`)
|
||||
|
||||
const paths = splitList(m.SystemPackagePath || "")
|
||||
println("")
|
||||
println("The following files would be deleted:")
|
||||
if (paths.length === 0) {
|
||||
println(" (manifest declares no files)")
|
||||
}
|
||||
paths.forEach(p => {
|
||||
const expanded = expandSystemPath(p)
|
||||
if (expanded.length === 0) {
|
||||
println(` (no match on disk) ${p}`)
|
||||
}
|
||||
else {
|
||||
expanded.forEach(e => println(` ${e}`))
|
||||
}
|
||||
})
|
||||
println(` ${m._manifestPath}`)
|
||||
|
||||
println("")
|
||||
println("(dry-run: no files were actually deleted)")
|
||||
return 0
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Dispatch
|
||||
// ============================================================
|
||||
|
||||
function printUsage() {
|
||||
println("Hopper - Package manager for TVDOS")
|
||||
println("")
|
||||
println("Usage:")
|
||||
println(" hopper {search,se} [--provides|--requires|--description|--author] <query>")
|
||||
println(" hopper {install,in} <package> [-v <version>]")
|
||||
println(" hopper {remove,rm} <package>")
|
||||
}
|
||||
|
||||
const _hopperArgs = (typeof exec_args !== "undefined" && exec_args) ? exec_args.slice(1) : []
|
||||
const _hopperCmd = _hopperArgs[0]
|
||||
const _hopperRest = _hopperArgs.slice(1)
|
||||
|
||||
switch (_hopperCmd) {
|
||||
case "search":
|
||||
case "se":
|
||||
return cmdSearch(_hopperRest)
|
||||
case "install":
|
||||
case "in":
|
||||
return cmdInstall(_hopperRest)
|
||||
case "remove":
|
||||
case "rm":
|
||||
return cmdRemove(_hopperRest)
|
||||
case undefined:
|
||||
printUsage()
|
||||
return 0
|
||||
default:
|
||||
printerrln(`Unknown command: ${_hopperCmd}`)
|
||||
printUsage()
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -3781,7 +3781,6 @@ function openFlagsPopup() {
|
||||
while (!done) {
|
||||
input.withEvent(ev => {
|
||||
if (ev[0] !== 'key_down') return
|
||||
if (1 !== ev[2]) return
|
||||
const ks = ev[1]
|
||||
if (eventJustReceived) { eventJustReceived = false; return }
|
||||
|
||||
|
||||
11
assets/disk0/tvdos/hopper/getopt.hop.per
Normal file
11
assets/disk0/tvdos/hopper/getopt.hop.per
Normal file
@@ -0,0 +1,11 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:getopt
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:getopt;
|
||||
HopperRequires:
|
||||
ProperName:getopt.js
|
||||
ProperAuthor:David Pacheco
|
||||
ProperDescription:node.js implementation of POSIX getopt() (and then some)
|
||||
Licence:MIT
|
||||
SystemPackagePath:/tvdos/include/getopt.mjs
|
||||
12
assets/disk0/tvdos/hopper/libfs.hop.per
Normal file
12
assets/disk0/tvdos/hopper/libfs.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:libfs
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:libfs;
|
||||
HopperRequires:tvdos 1.*;
|
||||
ProperName:LibFS
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:NodeJS-compatible Filesystem module for TVDOS
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/include/fs.mjs
|
||||
12
assets/disk0/tvdos/hopper/libgl.hop.per
Normal file
12
assets/disk0/tvdos/hopper/libgl.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:libgl
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:libgl;
|
||||
HopperRequires:
|
||||
ProperName:LibGL
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:TVDOS Graphics Library
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/include/gl.mjs
|
||||
12
assets/disk0/tvdos/hopper/libpcm.hop.per
Normal file
12
assets/disk0/tvdos/hopper/libpcm.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:libpcm
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:libpcm;
|
||||
HopperRequires:
|
||||
ProperName:LibPCM
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:PCM decoder for TSVM
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/include/pcm.mjs
|
||||
12
assets/disk0/tvdos/hopper/libpsg.hop.per
Normal file
12
assets/disk0/tvdos/hopper/libpsg.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:libpsg
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:libpsg;
|
||||
HopperRequires:
|
||||
ProperName:LibPSG
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:Programmable sound generator library for TSVM
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/include/psg.mjs
|
||||
12
assets/disk0/tvdos/hopper/libseqread.hop.per
Normal file
12
assets/disk0/tvdos/hopper/libseqread.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:libseqread
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:libseqread;
|
||||
HopperRequires:tvdos 1.*;
|
||||
ProperName:LibSeqread
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:Sequentially read files from disk drive
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/include/seqread.mjs;/tvdos/include/seqreadtape.mjs
|
||||
12
assets/disk0/tvdos/hopper/libtaud.hop.per
Normal file
12
assets/disk0/tvdos/hopper/libtaud.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:libtaud
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:libtaud;
|
||||
HopperRequires:tvdos 1.*;
|
||||
ProperName:LibTaud
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:Helper functions for interaction between Taud format and TSVM Tracker
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/include/taud.mjs
|
||||
12
assets/disk0/tvdos/hopper/libterranbasic.hop.per
Normal file
12
assets/disk0/tvdos/hopper/libterranbasic.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:libterranbasic
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:libterranbasic;
|
||||
HopperRequires:
|
||||
ProperName:LibTerranBasic
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:Terran BASIC runtime helper for compiled programs
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/include/tbas.mjs
|
||||
12
assets/disk0/tvdos/hopper/microtone.hop.per
Normal file
12
assets/disk0/tvdos/hopper/microtone.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:microtone
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:microtone;
|
||||
HopperRequires:tvdos 1.*;wintex 1.*;libtaud 1.*;libgl 1.*
|
||||
ProperName:Microtone
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:Microtonal tracker for TSVM
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/bin/microtone.alias;/tvdos/bin/taut*
|
||||
12
assets/disk0/tvdos/hopper/tvdos.hop.per
Normal file
12
assets/disk0/tvdos/hopper/tvdos.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:tvdos
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:tvdos;
|
||||
HopperRequires:
|
||||
ProperName:TVDOS
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:TSVM Disk Operating System
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/TVDOS.SYS
|
||||
12
assets/disk0/tvdos/hopper/wintex.hop.per
Normal file
12
assets/disk0/tvdos/hopper/wintex.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:wintex
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:wintex;
|
||||
HopperRequires:
|
||||
ProperName:WinTex
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:TUI window management and renderer
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/include/wintex.mjs
|
||||
12
assets/disk0/tvdos/hopper/zfm.hop.per
Normal file
12
assets/disk0/tvdos/hopper/zfm.hop.per
Normal file
@@ -0,0 +1,12 @@
|
||||
HopperManifestVersion:1
|
||||
HopperPackageName:zfm
|
||||
HopperPackageVersion:1.0.0
|
||||
HopperPackageMaintainer:CuriousTorvald
|
||||
HopperProvides:zfm;
|
||||
HopperRequires:tvdos 1.*;wintex 1.*
|
||||
ProperName:ZFM
|
||||
ProperAuthor:CuriousTorvald
|
||||
ProperDescription:Z File Manager - Dual-panel file manager for TVDOS
|
||||
Licence:MIT
|
||||
SupportMe:https://github.com/sponsors/curioustorvald/
|
||||
SystemPackagePath:/tvdos/bin/zfm*
|
||||
Reference in New Issue
Block a user