prog8/examples/cx16/fileselector.p8
Irmen de Jong a76b8d66ff cx16: added syslib.get_charset()
updated fileselector
2025-01-04 01:02:43 +01:00

483 lines
16 KiB
Lua

%import diskio
%import textio
%import strings
%zeropage basicsafe
%option no_sysinit
; A "TUI" for an interactive file selector, that scrolls the selection list if it doesn't fit on the screen.
; Returns the name of the selected file. If it is a directory instead, the name will start and end with a slash '/'.
; Functions in PETSCII mode and in ISO mode as well (no case folding in ISO mode!)
; Depends a lot on diskio routines, and uses the drive set in the diskio.drivenumber variable (usually just 8)
; should case folding be done in diskio already? -> no, it doesn't know if you are in iso mode or not.
; TODO use "@$:=p" instead of filtering manually for only dirs use @$:=d , but that needs a change in diskio...
; TODO is there a way to detect if iso charset mode is active (not really, except read the kernal variable...)
; TODO joystick control? mouse control?
; TODO keyboard typing; jump to the first entry that starts with that character? (but 'q' for quit stops working then, plus scrolling with pageup/down is already pretty fast)
main {
sub start() {
; some configuration, optional
fileselector.configure_settings(%00000011, 2)
fileselector.configure_appearance(10, 10, 20, $b3, $d0)
; show all files, using just the * wildcard
uword chosen = fileselector.select("*")
txt.nl()
txt.nl()
if chosen!=0 {
txt.print("chosen: ")
txt.print(chosen)
txt.nl()
} else {
txt.print("nothing chosen or error!\n")
txt.print(diskio.status())
}
}
}
fileselector {
%option ignore_unused
const uword filenamesbuffer = $a000 ; use a HIRAM bank
const uword filenamesbuf_size = $1e00 ; leaves room for a 256 entry string pointer table at $be00-$bfff
const uword filename_ptrs_start = $be00 ; array of 256 string pointers for each of the names in the buffer. ends with $0000.
ubyte dialog_topx = 10
ubyte dialog_topy = 10
ubyte max_lines = 20
ubyte colors_normal = $b3
ubyte colors_selected = $d0
ubyte buffer_rambank = 1 ; default hiram bank to use for the data buffers
ubyte show_what = 3 ; dirs and files
ubyte chr_topleft, chr_topright, chr_botleft, chr_botright, chr_horiz_top, chr_horiz_other, chr_vert, chr_jointleft, chr_jointright
ubyte num_visible_files
uword name_ptr
sub configure_settings(ubyte show_types, ubyte rambank) {
; show_types is a bit mask , bit 0 = show files, bit 1 = show dirs
buffer_rambank = rambank
show_what = show_types
set_characters(false)
}
sub configure_appearance(ubyte column, ubyte row, ubyte max_entries, ubyte normal, ubyte selected) {
dialog_topx = column
dialog_topy = row
max_lines = max_entries
colors_normal = normal
colors_selected = selected
}
sub select(str pattern) -> uword {
ubyte old_bank = cx16.getrambank()
cx16.rambank(buffer_rambank)
defer cx16.rambank(old_bank)
num_visible_files = 0
diskio.list_filename[0] = 0
name_ptr = diskio.diskname()
if name_ptr==0 or cbm.READST()!=0
return 0
bool iso_mode = cx16.get_charset()==1
set_characters(iso_mode)
txt.color2(colors_normal & 15, colors_normal>>4)
background(0, 3)
txt.plot(dialog_topx, dialog_topy)
txt.chrout(chr_topleft)
linepart(true)
txt.chrout(chr_topright)
txt.nl()
txt.column(dialog_topx)
txt.chrout(chr_vert)
txt.print(" drive ")
txt.print_ub(diskio.drivenumber)
txt.print(": '")
txt.print(name_ptr)
txt.chrout('\'')
txt.column(dialog_topx+31)
txt.chrout(chr_vert)
txt.nl()
txt.column(dialog_topx)
txt.chrout(chr_vert)
txt.print(" scanning directory... ")
txt.chrout(chr_vert)
txt.nl()
txt.column(dialog_topx)
footerline()
ubyte num_files = get_filenames(pattern, filenamesbuffer, filenamesbuf_size) ; use Hiram bank to store the files
ubyte selected_line
ubyte top_index
uword filename_ptrs
construct_name_ptr_array()
; sort alphabetically
sorting.shellsort_pointers(filename_ptrs_start, num_files)
num_visible_files = min(max_lines, num_files)
; initial display
background(5, 3 + num_visible_files -1)
txt.plot(dialog_topx+2, dialog_topy+2)
txt.print("select ")
if show_what & 1 == 1
txt.print("file")
else
txt.print("directory")
txt.print(": (")
txt.print_ub(num_files)
txt.print(" total)")
txt.column(dialog_topx+31)
txt.chrout(chr_vert)
txt.nl()
txt.column(dialog_topx)
txt.chrout(chr_vert)
txt.print(" esc/stop to abort ")
txt.chrout(chr_vert)
txt.nl()
txt.column(dialog_topx)
txt.chrout(chr_jointleft)
linepart(false)
txt.chrout(chr_jointright)
txt.nl()
print_scroll_indicator(false, true)
if num_files>0 {
for selected_line in 0 to num_visible_files-1 {
txt.column(dialog_topx)
txt.chrout(chr_vert)
txt.spc()
print_filename(peekw(filename_ptrs_start+selected_line*$0002))
txt.column(dialog_topx+31)
txt.chrout(chr_vert)
txt.nl()
}
} else {
txt.column(dialog_topx)
txt.chrout(chr_vert)
txt.print(" no matches.")
txt.column(dialog_topx+31)
txt.chrout(chr_vert)
txt.nl()
}
print_scroll_indicator(false, false)
txt.column(dialog_topx)
footerline()
selected_line = 0
select_line(0)
print_up_and_down()
repeat {
if cbm.STOP2()
return 0
ubyte key = cbm.GETIN2()
when key {
3, 27 -> return 0 ; STOP and ESC aborts
'\n',' ' -> {
if num_files>0 {
void strings.copy(peekw(filename_ptrs_start + (top_index+selected_line)*$0002), &diskio.list_filename)
return diskio.list_filename
}
return 0
}
'[',130,157 -> { ; PAGEUP, cursor left
; previous page of lines
unselect_line(selected_line)
if selected_line==0
repeat max_lines scroll_list_backward()
selected_line = 0
select_line(0)
print_up_and_down()
}
']',2,29 -> { ; PAGEDOWN, cursor right
if num_files>0 {
; next page of lines
unselect_line(selected_line)
if selected_line == max_lines-1
repeat max_lines scroll_list_forward()
selected_line = num_visible_files-1
select_line(selected_line)
print_up_and_down()
}
}
17 -> { ; down
if num_files>0 {
unselect_line(selected_line)
if selected_line<num_visible_files-1
selected_line++
else if num_files>max_lines
scroll_list_forward()
select_line(selected_line)
print_up_and_down()
}
}
145 -> { ; up
unselect_line(selected_line)
if selected_line>0
selected_line--
else if num_files>max_lines
scroll_list_backward()
select_line(selected_line)
print_up_and_down()
}
}
}
ubyte x,y
sub construct_name_ptr_array() {
filename_ptrs = filename_ptrs_start
name_ptr = filenamesbuffer
repeat num_files {
pokew(filename_ptrs, name_ptr)
if iso_mode {
; no case folding in iso mode
while @(name_ptr)!=0
name_ptr++
} else {
; case-folding to avoid petscii shifted characters coming out as symbols TODO should diskio do this already?
name_ptr += strings.lower(name_ptr)
}
name_ptr++
filename_ptrs+=2
}
pokew(filename_ptrs, 0)
}
sub print_filename(uword name) {
repeat 28 { ; maximum length displayed
if @(name)==0
break
txt.chrout(128) ; don't print control characters
txt.chrout(@(name))
name++
}
}
sub scroll_list_forward() {
if top_index+max_lines< num_files {
top_index++
; scroll the displayed list up 1
scroll_txt_up(dialog_topx+2, dialog_topy+6, 28, max_lines, sc:' ')
; print new name at the bottom of the list
txt.plot(dialog_topx+2, dialog_topy+6+max_lines-1)
print_filename(peekw(filename_ptrs_start + (top_index+ selected_line)*$0002))
}
}
sub scroll_list_backward() {
if top_index>0 {
top_index--
; scroll the displayed list down 1
scroll_txt_down(dialog_topx+2, dialog_topy+6, 28, max_lines, sc:' ')
; print new name at the top of the list
txt.plot(dialog_topx+2, dialog_topy+6)
print_filename(peekw(filename_ptrs_start + top_index * $0002))
}
}
sub scroll_txt_up(ubyte col, ubyte row, ubyte width, ubyte height, ubyte fillchar) {
for y in row to row+height-2 {
for x in col to col+width-1 {
txt.setchr(x,y, txt.getchr(x, y+1))
}
}
y = row+height-1
for x in col to col+width-1 {
txt.setchr(x,y, fillchar)
}
}
sub scroll_txt_down(ubyte col, ubyte row, ubyte width, ubyte height, ubyte fillchar) {
for y in row+height-1 downto row+1 {
for x in col to col+width-1 {
txt.setchr(x,y, txt.getchr(x, y-1))
}
}
for x in col to col+width-1 {
txt.setchr(x,row, fillchar)
}
}
sub print_up_and_down() {
if num_files<=max_lines
return
print_scroll_indicator(top_index>0, true)
print_scroll_indicator(top_index + num_visible_files < num_files, false)
}
sub print_scroll_indicator(bool visible, bool up) {
txt.plot(dialog_topx, dialog_topy + (if up 5 else 6+num_visible_files))
txt.chrout(chr_vert)
txt.column(dialog_topx+24)
if visible
if up
txt.print(" (up)")
else
txt.print("(down)")
else
txt.print(" ")
txt.spc()
txt.chrout(chr_vert)
txt.nl()
}
sub footerline() {
txt.chrout(chr_botleft)
linepart(false)
txt.chrout(chr_botright)
}
sub linepart(bool top) {
cx16.r0L = chr_horiz_other
if top
cx16.r0L = chr_horiz_top
repeat 30 txt.chrout(cx16.r0L)
}
sub select_line(ubyte line) {
line_color(line, colors_selected)
}
sub unselect_line(ubyte line) {
line_color(line, colors_normal)
}
sub line_color(ubyte line, ubyte colors) {
cx16.r1L = dialog_topy+6+line
ubyte charpos
for charpos in dialog_topx+1 to dialog_topx+30 {
txt.setclr(charpos, cx16.r1L, colors)
}
}
}
sub set_characters(bool iso_chars) {
if iso_chars {
chr_topleft = iso:'í'
chr_topright = iso:'ì'
chr_botleft = iso:'`'
chr_botright = iso:'\''
chr_jointleft = chr_jointright = iso:'÷'
chr_vert = iso:'|'
chr_horiz_top = iso:'¯'
chr_horiz_other = iso:'-'
} else {
; PETSCII box symbols
chr_topleft = ''
chr_topright = ''
chr_botleft = ''
chr_botright = ''
chr_horiz_top = ''
chr_horiz_other = ''
chr_vert = ''
chr_jointleft = ''
chr_jointright = ''
}
}
sub background(ubyte startrow, ubyte numlines) {
startrow += dialog_topy
repeat numlines {
txt.plot(dialog_topx+1, startrow)
repeat 30 txt.chrout(' ')
txt.nl()
startrow++
}
}
sub get_filenames(uword pattern_ptr, uword filenames_buffer, uword filenames_buf_size) -> ubyte {
uword buffer_start = filenames_buffer
ubyte files_found = 0
filenames_buffer[0]=0
if diskio.lf_start_list(pattern_ptr) {
while diskio.lf_next_entry() {
bool is_dir = diskio.list_filetype=="dir"
if is_dir and show_what & 2 == 0
continue
if not is_dir and show_what & 1 == 0
continue
if is_dir {
@(filenames_buffer) = '/' ; directories start with a slash so they're grouped when sorting
filenames_buffer++
}
filenames_buffer += strings.copy(diskio.list_filename, filenames_buffer)
if is_dir {
@(filenames_buffer) = '/' ; directories also end with a slash
filenames_buffer++
@(filenames_buffer) = 0
}
filenames_buffer++
files_found++
if filenames_buffer - buffer_start > filenames_buf_size-20 {
@(filenames_buffer)=0
diskio.lf_end_list()
sys.set_carry()
return files_found
}
}
diskio.lf_end_list()
}
@(filenames_buffer)=0
sys.clear_carry()
return files_found
}
}
sorting {
; note: cannot use the sorting library module because that relies on zeropage to be directly available (@requirezp pointer)
; and this code is meant to be able to being used without zeropage as well (except for R0-R15).
sub shellsort_pointers(uword stringpointers_array, ubyte num_elements) {
; Comparefunc must be a routine that accepts 2 pointers in R0 and R1, and must return with Carry=1 if R0<=R1, otherwise Carry=0.
; One such function, to compare strings, is provided as 'string_comparator' below.
cx16.r2 = stringpointers_array ; need zeropage pointer
num_elements--
ubyte gap
for gap in [132, 57, 23, 10, 4, 1] {
ubyte i
for i in gap to num_elements {
cx16.r1 = peekw(cx16.r2+i*$0002)
ubyte @zp j = i
ubyte @zp k = j-gap
while j>=gap {
cx16.r0 = peekw(cx16.r2+k*2)
if string_comparator(cx16.r0, cx16.r1)
break
pokew(cx16.r2+j*2, cx16.r0)
j = k
k -= gap
}
pokew(cx16.r2+j*2, cx16.r1)
}
}
asmsub string_comparator(uword string1 @R0, uword string2 @R1) -> bool @Pc {
; R0 and R1 are the two strings, must return Carry=1 when R0<=R1, else Carry=0
%asm {{
lda cx16.r1L
ldy cx16.r1H
sta P8ZP_SCRATCH_W2
sty P8ZP_SCRATCH_W2+1
lda cx16.r0L
ldy cx16.r0H
jsr prog8_lib.strcmp_mem
cmp #1
bne +
clc
rts
+ sec
rts
}}
}
}
}