farjs-app
Version:
FAR.js - Cross-platform File and Archive Manager app in your terminal
360 lines (324 loc) • 9.32 kB
JavaScript
/**
* @typedef {import("./ViewerFileLine.mjs").ViewerFileLine} ViewerFileLine
*/
import UiString from "@farjs/ui/UiString.mjs";
import Encoding from "../file/Encoding.mjs";
import ViewerFileLine from "./ViewerFileLine.mjs";
import ViewerFileReader from "./ViewerFileReader.mjs";
/**
* @typedef {{
* readonly encoding: string;
* readonly size: number;
* readonly width: number;
* readonly height: number;
* readonly wrap: boolean;
* readonly column: number;
* readonly position: number;
* readonly linesData: readonly ViewerFileLine[];
* }} ViewerFileViewportData
*/
/**
* @typedef {ViewerFileViewportData & {
* readonly fileReader: ViewerFileReader;
* readonly content: string;
* readonly scrollIndicators: readonly number[];
* readonly progress: number;
* moveUp(lines: number, from?: number): Promise<ViewerFileViewport>;
* moveDown(lines: number): Promise<ViewerFileViewport>;
* reload(from?: number): Promise<ViewerFileViewport>;
* updated(data: Partial<ViewerFileViewportData>): ViewerFileViewport;
* }} ViewerFileViewport
*/
/**
* @implements {ViewerFileViewport}
*/
class ViewerFileViewportImpl {
/**
* @param {ViewerFileReader} fileReader
* @param {string} encoding
* @param {number} size
* @param {number} width
* @param {number} height
* @param {boolean} wrap
* @param {number} column
* @param {number} position
* @param {readonly ViewerFileLine[]} linesData
*/
constructor(
fileReader,
encoding,
size,
width,
height,
wrap,
column,
position,
linesData
) {
/** @readonly @type {ViewerFileReader} */ this.fileReader = fileReader;
/** @readonly @type {string} */ this.encoding = encoding;
/** @readonly @type {number} */ this.size = size;
/** @readonly @type {number} */ this.width = width;
/** @readonly @type {number} */ this.height = height;
/** @readonly @type {boolean} */ this.wrap = wrap;
/** @readonly @type {number} */ this.column = column;
/** @readonly @type {number} */ this.position = position;
/** @readonly @type {readonly ViewerFileLine[]} */ this.linesData =
linesData;
/** @private @type {string | undefined} */ this._content = undefined;
/** @private @type {readonly number[] | undefined} */ this._scrollIndicators =
undefined;
/** @private @type {number | undefined} */ this._progress = undefined;
}
/**
* @returns {string}
*/
get content() {
if (this._content === undefined) {
/** @type {string[]} */
const buf = [];
this.linesData.forEach((line) => {
const lineStr = UiString(line.line).slice(
this.column,
this.column + this.width
);
const chars = Array.from(lineStr);
for (let j = 0; j < chars.length; j += 1) {
const str = chars[j];
const ch = str.charCodeAt(0);
const isControl =
ch === 0x00 || // nul
ch === 0x07 || // bel
ch === 0x08 || // backspace
ch === 0x0b || // vertical tab
ch === 0x1b || // ESC
ch === 0x7f; // <DEL>
chars[j] =
isControl && str !== "\t" && str !== "\r" && str !== "\n"
? " "
: str;
}
buf.push(chars.join(""), "\n");
});
this._content = buf.join("");
}
return this._content;
}
/**
* @returns {readonly number[]}
*/
get scrollIndicators() {
if (this._scrollIndicators === undefined) {
const maybeIndexes = this.linesData.map((line, index) => {
return UiString(line.line).strWidth() > this.column + this.width
? index
: undefined;
});
this._scrollIndicators = maybeIndexes.filter((_) => _ !== undefined);
}
return this._scrollIndicators;
}
/**
* @returns {number}
*/
get progress() {
if (this._progress === undefined) {
const bytes = this.linesData.reduce((res, line) => res + line.bytes, 0);
const viewed = this.position + bytes;
this._progress =
this.size === 0.0 ? 0 : Math.trunc((viewed / this.size) * 100);
}
return this._progress;
}
/**
* @param {number} lines
* @param {number} [from]
* @returns {Promise<ViewerFileViewport>}
*/
async moveUp(lines, from = this.position) {
if (from === 0.0) {
return this;
}
const data = await this.fileReader
.readPrevLines(lines, from, this.size, this.encoding)
.then(this._doWrap(lines, true));
if (data.length > 0) {
const bytes = data.reduce((sum, _) => sum + _.bytes, 0);
if (from < this.size) {
data.push(...this.linesData);
}
return this.updated({
position: Math.max(from - bytes, 0),
linesData: data.slice(0, Math.min(this.height, data.length)),
});
}
return this;
}
/**
* @param {number} lines
* @returns {Promise<ViewerFileViewport>}
*/
async moveDown(lines) {
const bytes = this.linesData.reduce((sum, _) => sum + _.bytes, 0);
const nextPosition = this.position + bytes;
if (nextPosition >= this.size) {
return this.linesData.length === this.height
? this.moveUp(this.height, this.size)
: Promise.resolve(this);
}
const data = await this.fileReader
.readNextLines(lines, nextPosition, this.encoding)
.then(this._doWrap(lines, false));
if (data.length > 0) {
const bytes = this.linesData
.slice(0, Math.min(lines, this.linesData.length))
.reduce((sum, _) => sum + _.bytes, 0);
const res = this.linesData.slice(Math.min(lines, this.linesData.length));
res.push(...data);
return this.updated({
position: this.position + bytes,
linesData: res,
});
}
return this;
}
/**
* @param {number} [from]
* @returns {Promise<ViewerFileViewport>}
*/
reload(from = this.position) {
return this.fileReader
.readNextLines(this.height, from, this.encoding)
.then(this._doWrap(this.height, false))
.then((linesData) => {
return this.updated({
position: from,
linesData,
});
});
}
/**
* @param {number} lines
* @param {boolean} up
* @returns {(data: ViewerFileLine[]) => ViewerFileLine[]}
*/
_doWrap(lines, up) {
const encoding = this.encoding;
const width = this.width;
return (data) => {
if (!this.wrap) {
return data;
}
/** @type {ViewerFileLine[]} */
const res = [];
/** @type {(fileLine: ViewerFileLine) => void} */
function loop(fileLine) {
while (true) {
const { line, bytes } = fileLine;
if (line.length <= width) {
res.push(fileLine);
return;
}
const [wrapped, rest] = (() => {
if (up) {
return [
//takeRight
line.slice(
Math.min(line.length - Math.max(width, 0), line.length),
line.length
),
//dropRight
line.slice(
0,
Math.min(line.length - Math.max(width, 0), line.length)
),
];
}
return [
//take
line.slice(0, Math.min(width, line.length)),
//drop
line.slice(Math.min(width, line.length), line.length),
];
})();
const wrappedBytes = Encoding.byteLength(wrapped, encoding);
res.push(ViewerFileLine(wrapped, wrappedBytes));
fileLine = ViewerFileLine(rest, Math.max(bytes - wrappedBytes, 0));
}
}
if (up) {
data.reduceRight((_, d) => loop(d), /** @type {void} */ (undefined));
const r = res.reverse();
//takeRight
return r.slice(
Math.min(r.length - Math.max(lines, 0), r.length),
r.length
);
}
data.reduce((_, d) => loop(d), /** @type {void} */ (undefined));
//take
return res.slice(0, Math.min(lines, res.length));
};
}
/**
* @param {Partial<ViewerFileViewportData>} data
* @returns {ViewerFileViewport}
*/
updated({
encoding,
size,
width,
height,
wrap,
column,
position,
linesData,
}) {
return new ViewerFileViewportImpl(
this.fileReader,
encoding ?? this.encoding,
size ?? this.size,
width ?? this.width,
height ?? this.height,
wrap ?? this.wrap,
column ?? this.column,
position ?? this.position,
linesData ?? this.linesData
);
}
}
/**
* @param {ViewerFileReader} fileReader
* @param {string} encoding
* @param {number} size
* @param {number} width
* @param {number} height
* @param {boolean} [wrap]
* @param {number} [column]
* @param {number} [position]
* @param {readonly ViewerFileLine[]} [linesData]
* @returns {ViewerFileViewport}
*/
export function createViewerFileViewport(
fileReader,
encoding,
size,
width,
height,
wrap = false,
column = 0,
position = 0,
linesData = []
) {
return new ViewerFileViewportImpl(
fileReader,
encoding,
size,
width,
height,
wrap,
column,
position,
linesData
);
}