list<pair<int, string>>
TMDTag;
51 typedeflist<pair<int, char>>
TCigar;
53 template<
typenameT>
77 while(!
m_md.empty() &&
m_md.front().first < 0)
83 if(
m_cigar.front().second ==
'D')
94 while(!
m_md.empty() &&
m_md.back().first < 0)
100 if(
m_cigar.back().second ==
'D')
132 Exon GetNextExon(
string& cigar_string,
string& MD_tag,
intqfrom,
intsfrom);
169 while(!cigar_string.empty()) {
172 charc = cigar_string[
letter];
175 if(c !=
'S'&& c !=
'N')
179cigar_string.erase(0,
letter+1);
186}
else if(c ==
'N') {
194}
else if(c ==
'M'|| c ==
'='|| c ==
'X') {
200 if(
isdigit(MD_tag.front())) {
202 intm = stoi(MD_tag, &idx);
203 if(e.
m_md.empty() || e.
m_md.back().first <= 0)
207MD_tag.erase(0, idx);
209MD_tag = to_string(m-
len)+MD_tag;
219 if(e.
m_md.empty() || e.
m_md.back().first != 0)
220e.
m_md.emplace_back(0, MD_tag.substr(0, 1));
222e.
m_md.back().second += MD_tag.front();
229}
else if(c ==
'I') {
235}
else if(c ==
'D') {
241 if(MD_tag.front() ==
'^') {
242del = MD_tag.substr(1,
len);
243MD_tag.erase(0,
len+1);
245del = MD_tag.substr(0,
len);
246MD_tag.erase(0,
len);
248 if(e.
m_md.empty() || e.
m_md.back().first >= 0)
249e.
m_md.emplace_back(-1, del);
251e.
m_md.back().second += del;
254cerr <<
"Unexpected symbol in CIGAR"<< endl;
263 intlength = seq.size();
272 case 'A': tA += 1;
break;
273 case 'C': tC += 1;
break;
274 case 'G': tG += 1;
break;
275 case 'T': tT += 1;
break;
279 doubleentropy = -(tA*
log(tA/length)+tC*
log(tC/length)+tG*
log(tG/length)+tT*
log(tT/length))/(length*
log(4.));
288 for(
int i= 11;
i< (
int)tags.size() && (MD_tag.empty() || tstrand.empty()); ++
i) {
289 auto&
tag= tags[
i];
290vector<string> fields;
292 if(fields.size() != 3)
294 if(fields[0] ==
"MD"&& fields[1] ==
"Z")
296 if((fields[0] ==
"ts"|| fields[0] ==
"TS"|| fields[0] ==
"XS") && fields[1] ==
"A")
299 if(MD_tag.empty()) {
300cerr <<
"MD:Z tag in SAM file is expected"<< endl;
305 stringcigar_string = tags[5];
307 intsfrom = std::stoi(tags[3])-1;
308 while(!cigar_string.empty()) {
309align_exons.push_back(
GetNextExon(cigar_string, MD_tag, qfrom, sfrom));
310align_exons.back().m_tstrand = tstrand;
311qfrom = align_exons.back().m_qto+1;
312sfrom = align_exons.back().m_sto+1;
316 const string& seq = tags[9];
317 while(!align_exons.empty() &&
Entropy(seq.substr(align_exons.front().m_qfrom, align_exons.front().m_qto-align_exons.front().m_qfrom+1)) <
m_min_entropy)
318align_exons.erase(align_exons.begin());
319 while(!align_exons.empty() &&
Entropy(seq.substr(align_exons.back().m_qfrom, align_exons.back().m_qto-align_exons.back().m_qfrom+1)) <
m_min_entropy)
320align_exons.pop_back();
322exons.insert(exons.end(), align_exons.begin(), align_exons.end());
326 returnname+
":i:"+to_string(
value);
328 string Tag(
const string& name,
const string&
value) {
329 returnname+
":Z:"+
value;
332 returnname+
":A:"+
value;
337fields[0] = compart.
m_read;
338 intflag = 1+2+strand*16+(1-strand)*32+(mate+1)*64;
339fields[1] = to_string(flag);
341fields[3] = to_string(compart.
m_range.first+1);
349fields[8] = to_string(span);
350fields[9] = compart.
m_rseq;
352fields.push_back(
Tag(
"NM", compart.
m_dist));
356fields.push_back(
Tag(
"MD", compart.
m_md));
357fields.push_back(
Tag(
"NH",
count));
358fields.push_back(
Tag(
"HI", index));
362 for(
unsigned i= 1;
i< fields.size(); ++
i)
363cout <<
"\t"<< fields[
i];
369fields[0] = compart.
m_read;
370 intflag = strand*16;
372flag += 1+8+(mate+1)*64;
373fields[1] = to_string(flag);
375fields[3] = to_string(compart.
m_range.first+1);
381fields[9] = compart.
m_rseq;
384fields.push_back(
Tag(
"NM", compart.
m_dist));
385fields.push_back(
Tag(
"AS", compart.
m_score));
388fields.push_back(
Tag(
"MD", compart.
m_md));
389fields.push_back(
Tag(
"NH",
count));
390fields.push_back(
Tag(
"HI", index));
393 for(
unsigned i= 1;
i< fields.size(); ++
i)
394cout <<
"\t"<< fields[
i];
399 boolpaired =
false;
402 for(
intmate = 0; mate < 2; ++mate) {
403 for(
intstrand = 0; strand < 2; ++strand) {
405 for(
auto& compart : compact_aligns) {
407 if(compart.m_matep !=
nullptr)
417 auto& contig = contig_aligns.first;
418 for(
intmate = 0; mate < 2; ++mate) {
420 auto& left_aligns = contig_aligns.second[mate][strand];
421 for(
auto& compart : left_aligns) {
422 FormatPaired(compart, contig, mate, strand, mate_count[0], ++index,
true);
423 FormatPaired(*compart.m_matep, contig, 1-mate, 1-strand, mate_count[0], index,
false);
430 auto& contig = contig_aligns.first;
431 for(
intmate = 0; mate < 2; ++mate) {
432 for(
intstrand = 0; strand < 2; ++strand) {
434 for(
auto& compart : compact_aligns)
435 FormatSingle(compart, contig, mate, strand, mate_count[mate], ++mate_index[mate]);
446 for(
intmate = 0; mate < 2; ++mate) {
447 for(
intstrand = 0; strand < 2; ++strand) {
449 for(
auto& align : compact_aligns) {
450mate_score[mate] =
max(mate_score[mate], align.m_score);
451 if(align.m_matep !=
nullptr)
452pair_score =
max(pair_score, align.m_score+align.m_matep->m_score);
454compact_aligns.remove_if([](
const AlignCompartment&
a){
return!
a.m_above_thresholds; });
459 boolpaired = pair_score >=
max(mate_score[0], mate_score[1]);
461 for(
intmate = 0; mate < 2; ++mate) {
462 for(
intstrand = 0; strand < 2; ++strand) {
466 if(
a.m_matep ==
nullptr)
return true;
467 if(
a.m_score+
a.m_matep->m_score == pair_score)
return false;
468 a.m_matep->m_matep =
nullptr;
return true; });
470compact_aligns.remove_if([&](
AlignCompartment&
a){
a.m_matep =
nullptr;
return a.m_score != mate_score[mate]; });
488vector<pair<int,int>> left_introns;
489vector<pair<int,int>> right_introns;
491 for(
unsigned i= 1;
i< left.
m_exons.size(); ++
i) {
493left_introns.emplace_back(left.
m_exons[
i-1].m_sto+1, left.
m_exons[
i].m_sfrom-1);
495 for(
unsigned i= 1;
i< right.
m_exons.size(); ++
i) {
497right_introns.emplace_back(right.
m_exons[
i-1].m_sto+1, right.
m_exons[
i].m_sfrom-1);
499 returnleft_introns == right_introns;
504 for(
intleft_mate = 0; left_mate < 2; ++left_mate) {
505 intright_mate = 1-left_mate;
507 intright_strand = 1;
508 auto& left_aligns = contig_aligns.second[left_mate][left_strand];
509 auto& right_aligns = contig_aligns.second[right_mate][right_strand];
511 autoiright = right_aligns.begin();
512 for(
autoileft = left_aligns.begin(); ileft != left_aligns.end() && iright != right_aligns.end(); ++ileft) {
513 while(iright != right_aligns.end() && iright->m_range.second < ileft->m_range.second)
515 if(iright == right_aligns.end())
517 autoinext =
next(ileft);
518 if(inext != left_aligns.end() && inext->m_range.second < iright->m_range.second)
523ileft->m_matep = &(*iright);
524iright->m_matep = &(*ileft);
536 auto& contig = contig_aligns.first;
537 for(
intmate = 0; mate < 2; ++mate) {
538 for(
intstrand = 0; strand < 2; ++strand) {
539 TSplitAligns& orig_aligns = contig_aligns.second[mate][strand];
540 if(orig_aligns.empty())
542 string& read = orig_aligns.front()[0];
543 boolpaired_read = stoi(orig_aligns.front()[1])&1;
544 string& rseq = orig_aligns.front()[9];
545 intread_len = rseq.size();
549 for(
auto& oa : orig_aligns)
555 sort(exons.begin(), exons.end(), [](
const Exon& e1,
const Exon& e2){
556if(e1.m_sto != e2.m_sto)
557return e1.m_sto > e2.m_sto;
558if(e1.m_sfrom != e2.m_sfrom)
559return e1.m_sfrom < e2.m_sfrom;
560if(e1.m_qto != e2.m_qto)
561return e1.m_qto > e2.m_qto;
562if(e1.m_qfrom != e2.m_qfrom)
563return e1.m_qfrom < e2.m_qfrom;
564if(e1.m_score != e2.m_score)
565return e1.m_score > e2.m_score;
566return e1.m_cigar < e2.m_cigar; });
569 autotail = unique(exons.begin(), exons.end(), [](
const Exon& e1,
const Exon& e2){
570return e1.m_sfrom == e2.m_sfrom && e1.m_sto == e2.m_sto && e1.m_qfrom == e2.m_qfrom && e1.m_qto == e2.m_qto; });
571exons.erase(tail, exons.end());
574 for(
auto i= exons.begin();
i!= exons.end(); ++
i) {
575 if(
i->m_type ==
eBogus||
i->m_qfrom > 10)
577 for(
autoj =
next(
i); j != exons.end() && j->m_sto ==
i->m_sto && j->m_qto ==
i->m_qto; ++j)
580exons.erase(
remove_if(exons.begin(), exons.end(), [](
Exon& e){ return e.m_type == eBogus; }), exons.end());
582 sort(exons.begin(), exons.end(), [](
const Exon& e1,
const Exon& e2){
583if(e1.m_sfrom != e2.m_sfrom)
584return e1.m_sfrom < e2.m_sfrom;
585if(e1.m_sto != e2.m_sto)
586return e1.m_sto > e2.m_sto;
587if(e1.m_qfrom != e2.m_qfrom)
588return e1.m_qfrom < e2.m_qfrom;
589return e1.m_qto > e2.m_qto; });
592 for(
auto i= exons.begin();
i!= exons.end(); ++
i) {
593 if(
i->m_type ==
eBogus|| read_len-
i->m_qto-1 > 10)
595 for(
autoj =
next(
i); j != exons.end() && j->m_sfrom ==
i->m_sfrom && j->m_qfrom ==
i->m_qfrom; ++j)
598exons.erase(
remove_if(exons.begin(), exons.end(), [](
Exon& e){ return e.m_type == eBogus; }), exons.end());
603 for(
autoj =
next(
i); j != exons.end() && j->m_sfrom <=
i->m_sto && !
m_read_through; ++j) {
604 intsoverlap =
i->m_sto-j->m_sfrom+1;
605 intslen =
min(
i->m_sto-
i->m_sfrom+1, j->m_sto-j->m_sfrom+1);
606 intqoverlap =
i->m_qto-j->m_qfrom+1;
607 intqlen =
min(
i->m_qto-
i->m_qfrom+1, j->m_qto-j->m_qfrom+1);
608 if(soverlap > 0.5*slen && qoverlap < 0.1*qlen)
618 for(
auto& e : exons) {
624 doubleident = double(matches)/align_len;
625 doublecov = double(qmax-qmin+1)/read_len;
627 doublelog2factor = 0.25;
628 intcompart_penalty = ident*cov*
m_penalty*read_len + 0.5;
629 intexon_num = exons.size();
630 intbest_index = exon_num-1;
631 for(
int i= exon_num-1;
i>= 0; --
i) {
632 auto& ei = exons[
i];
633 intiscore = ei.m_matches;
635ei.m_chain_score = iscore;
637 for(
intj =
i+1; j < exon_num; ++j) {
638 auto& ej = exons[j];
639 intijscore = ej.m_chain_score+iscore;
643 if(ej.m_sfrom <= ei.m_sto)
646 intoverlap = ei.m_qto+1-ej.m_qfrom;
647 intintron = ej.m_sfrom-ei.m_sto-1;
648 if(overlap == 0 && intron > 20 && intron <=
m_max_intron) {
650 doubleintron_len = ej.m_intron_len+intron;
651 doubledeltas = score-ei.m_chain_score-log2factor*
log2((intron_len+1)/(ei.m_intron_len+1));
653ei.m_chain_score = score;
656ei.m_intron_len = intron_len;
659 intoverlap_penalty = 0;
660overlap_penalty =
max(2,
int(ident*
abs(overlap)+0.5));
661 intscore = ijscore-overlap_penalty;
662 doubleintron_len = ej.m_intron_len;
663 doubledeltas = score-ei.m_chain_score-log2factor*
log2((intron_len+1)/(ei.m_intron_len+1));
665ei.m_chain_score = score;
668ei.m_intron_len = intron_len+intron;
670}
else if(overlap > 0 && overlap < 10 && intron <=
m_max_intron) {
671 intoverlap_penalty = 0;
672 ints = ei.m_script.size()-1;
674 while(s >= 0 &&
l> 0) {
675 if(ei.m_script[s] ==
'=')
677 if(ei.m_script[s] !=
'D')
683 while(s < (
int)ej.m_script.size() &&
l> 0) {
684 if(ej.m_script[s] ==
'=')
686 if(ej.m_script[s] !=
'D')
690overlap_penalty =
max(2, overlap_penalty);
691 intscore = ijscore-overlap_penalty;
692 doubleintron_len = ej.m_intron_len+intron;
693 doubledeltas = score-ei.m_chain_score-log2factor*
log2((intron_len+1)/(ei.m_intron_len+1));
695ei.m_chain_score = score;
698ei.m_intron_len = intron_len;
701 intscore = ijscore-compart_penalty;
702 doubleintron_len = ej.m_intron_len;
703 doubledeltas = score-ei.m_chain_score-log2factor*
log2((intron_len+1)/(ei.m_intron_len+1));
705ei.m_chain_score = score;
708ei.m_intron_len = intron_len;
712 doubledeltas = ei.m_chain_score-exons[best_index].m_chain_score-log2factor*
log2((
double)(ei.m_intron_len+1)/(exons[best_index].m_intron_len+1));
728 for(
Exon* p = &exons[best_index]; p !=
nullptr; p = p->
m_prevp) {
729 if(compartments.empty() || compartments.back().m_exons.back().m_type !=
eExtension)
730compartments.emplace_back();
731compartments.back().m_exons.push_back(*p);
735 for(
autoiloop = compartments.begin(); iloop != compartments.end(); ) {
737 while(!it->m_exons.empty()) {
739 if(it->m_exons.front().m_align_len == 0)
740it->m_exons.erase(it->m_exons.begin());
744 while(!it->m_exons.empty()) {
746 if(it->m_exons.back().m_align_len == 0)
747it->m_exons.pop_back();
751 if(it->m_exons.empty())
752compartments.erase(it);
756 for(
auto& compartment : compartments) {
759 string& cigar = compartment.m_cigar;
760 if(compartment.m_exons.front().m_qfrom > 0)
761cigar += to_string(compartment.m_exons.front().m_qfrom)+
"S";
762 string& tstrand = compartment.m_tstrand;
763tstrand = compartment.m_exons.front().m_tstrand;
765 for(
int i= 0;
i< (
int)compartment.m_exons.size(); ++
i) {
766 auto& e = compartment.m_exons[
i];
771 intintron = compartment.m_exons[
i].m_sfrom-compartment.m_exons[
i-1].m_sto-1;
772cigar += to_string(intron)+
"N";
774 for(
auto& elem : compartment.m_exons[
i].m_cigar)
775cigar += to_string(elem.first)+elem.second;
777 if(!tstrand.empty() && e.
m_tstrand!= tstrand)
779md.insert(md.end(), e.
m_md.begin(), e.
m_md.end());
781 inttail = rseq.size()-compartment.m_exons.back().m_qto-1;
783cigar += to_string(tail)+
"S";
785 if(compartment.m_exons.size() == 1)
788 string& md_tag = compartment.m_md;
789 for(
auto i= md.begin();
i!= md.end(); ++
i) {
790 for(
autoj =
next(
i); j != md.end(); j =
next(
i)) {
791 if(
i->first > 0 && j->first > 0)
792 i->first += j->first;
793 else if(
i->first == j->first)
794 i->second += j->second;
800md_tag += to_string(
i->first);
801 else if(
i->first == 0)
802md_tag +=
i->second;
804md_tag +=
"^"+
i->second;
806compartment.m_range.first = compartment.m_exons.front().m_sfrom;
807compartment.m_range.second = compartment.m_exons.back().m_sto;
808 intread_span = compartment.m_exons.back().m_qto-compartment.m_exons.front().m_qfrom+1;
810compartment.m_read = read;
811compartment.m_rseq = rseq;
812compartment.m_paired_read = paired_read;
827list<TAlignCompartments::iterator> erased;
828TAlignCompartments::reverse_iterator keeper = compartments.rbegin();
829 for(
auto i=
next(keeper);
i!= compartments.rend(); ++
i) {
834 if(
i->m_score > keeper->m_score) {
835erased.push_back(
prev(keeper.base()));
838erased.push_back(
prev(
i.base()));
841 for(
auto i: erased)
842compartments.erase(
i);
866argdescr->SetUsageContext(
GetArguments().GetProgramBasename(),
"exon_selector expects SAM alignments at stdin collated by query, e.g. with 'sort -k 1,1'");
868argdescr->AddDefaultKey (
"max_intron",
"max_intron",
"Maximum intron length (in base pairs)",
CArgDescriptions::eInteger,
"1200000");
869argdescr->AddDefaultKey(
"min_query_len",
"min_query_len",
"Minimum length for individual cDNA sequences.",
CArgDescriptions::eInteger,
"50");
870argdescr->AddFlag(
"star",
"STAR aligner settings: -match 1 -mismatch 1 -gapopen1 2 -gapextend1 2 -gapopen2 2 -gapextend2 2 -min_output_identity 0.9 -min_output_coverage 0.9");
871argdescr->AddFlag(
"minimap2",
"minimap2 aligner settings: -match 1 -mismatch 2 -gapopen1 2 -gapextend1 1 -gapopen2 32 -gapextend2 0 -min_output_identity 0.8 -min_output_coverage 0.3");
872argdescr->AddOptionalKey(
"min_output_identity",
"min_output_identity",
"Minimal identity for output alignments",
CArgDescriptions::eDouble);
873argdescr->AddOptionalKey(
"min_output_coverage",
"min_output_coverage",
"Minimal coverage for output alignments",
CArgDescriptions::eDouble);
876argdescr->AddOptionalKey(
"gapopen1",
"gapopen1",
"Gap open penalty. A gap of length k costs min{gapopen1+k*gapextend1,gapopen2+k*gapextend2}",
CArgDescriptions::eInteger);
880argdescr->AddFlag(
"nocheck",
"Don't check if reads are collated (saves memory)");
881argdescr->AddFlag(
"remove_repeats",
"Remove alignments which have repeats in the read");
884argdescr->SetConstraint(
"penalty", constrain01);
885argdescr->SetConstraint(
"min_output_identity", constrain01);
886argdescr->SetConstraint(
"min_output_coverage", constrain01);
889argdescr->SetConstraint(
"min_query_len", constrain_minqlen);
892argdescr->SetConstraint(
"match", constrain_score);
893argdescr->SetConstraint(
"mismatch", constrain_score);
894argdescr->SetConstraint(
"gapopen1", constrain_score);
895argdescr->SetConstraint(
"gapextend1", constrain_score);
896argdescr->SetConstraint(
"gapopen2", constrain_score);
897argdescr->SetConstraint(
"gapextend2", constrain_score);
906 m_penalty= args[
"penalty"].AsDouble();
909 bool check= !args[
"nocheck"];
910 if(args[
"remove_repeats"])
913 if(args[
"star"] && args[
"minimap2"]) {
914cerr <<
"-star and -minimap2 can't be used together"<< endl;
918 if(args[
"minimap2"] || !args[
"star"]) {
929 if(args[
"star"]) {
940 if(args[
"min_output_identity"])
942 if(args[
"min_output_coverage"])
947 if(args[
"mismatch"])
949 if(args[
"gapopen1"])
951 if(args[
"gapextend1"])
953 if(args[
"gapopen2"])
955 if(args[
"gapextend2"])
960 boolpaired_read =
false;
962 while(getline(cin, line)) {
965 if(line[0] ==
'@') {
966cout << line << endl;
969vector<string> fields;
971 stringread = fields[0];
973 if(
check&& !previous_reads.
insert(read_old).second) {
974cerr <<
"Input collated by reads is expected"<< endl;
987paired_read = stoi(fields[1])&1;
993 stringcontig = fields[2];
994 intflag = std::stoi(fields[1]);
995 intstrand = (flag&16) ? 1 : 0;
996 intmate = (flag&128) ? 1 : 0;
1001 if(
check&& !previous_reads.
insert(read_old).second) {
1002cerr <<
"Input collated by reads is expected"<< endl;
1017 int main(
intargc,
const char* argv[])
void remove_if(Container &c, Predicate *__pred)
virtual void Init()
Initialize the application.
bool CompatiblePair(const AlignCompartment &left, const AlignCompartment &right)
map< string, TMatrix< TAlignCompartments > > m_compartments
list< pair< int, char > > TCigar
void FormatSingle(AlignCompartment &compart, const string &contig, int mate, int strand, int count, int index)
list< TSamFields > TSplitAligns
map< string, TMatrix< TSplitAligns > > m_read_aligns
void ExtractExonsFromSAM(const TSamFields &tags, TExons &exons)
Exon GetNextExon(string &cigar_string, string &MD_tag, int qfrom, int sfrom)
virtual int Run()
Run the application.
list< pair< int, string > > TMDTag
vector< string > TSamFields
void SelectBestLocations()
list< AlignCompartment > TAlignCompartments
void FormatPaired(AlignCompartment &compart, const string &contig, int mate, int strand, int count, int index, bool left)
iterator_bool insert(const value_type &val)
double Entropy(const string &seq)
string Tag(const string &name, int value)
int main(int argc, const char *argv[])
static DLIST_TYPE *DLIST_NAME() prev(DLIST_LIST_TYPE *list, DLIST_TYPE *item)
static DLIST_TYPE *DLIST_NAME() next(DLIST_LIST_TYPE *list, DLIST_TYPE *item)
virtual const CArgs & GetArgs(void) const
Get parsed command line arguments.
int AppMain(int argc, const char *const *argv, const char *const *envp=0, EAppDiagStream diag=eDS_Default, const char *conf=NcbiEmptyCStr, const string &name=NcbiEmptyString)
Main function (entry point) for the NCBI application.
virtual void SetupArgDescriptions(CArgDescriptions *arg_desc)
Setup the command line argument descriptions.
#define ITERATE(Type, Var, Cont)
ITERATE macro to sequence through container elements.
const CNcbiArguments & GetArguments(void) const
Get the application's cached unprocessed command-line arguments.
@ eDouble
Convertible into a floating point number (double)
@ eInteger
Convertible into an integer number (int or Int8)
EDiagSev SetDiagPostLevel(EDiagSev post_sev=eDiag_Error)
Set the threshold severity for posting the messages.
@ eDS_ToStderr
To standard error stream.
@ eDiag_Info
Informational message.
static list< string > & Split(const CTempString str, const CTempString delim, list< string > &arr, TSplitFlags flags=0, vector< SIZE_TYPE > *token_pos=NULL)
Split a string using specified delimiters.
unsigned int
A callback function used to compare two keys in a database.
constexpr auto sort(_Init &&init)
const struct ncbi::grid::netcache::search::fields::SIZE size
const GenericPointer< typename T::ValueType > T2 value
Defines the CNcbiApplication and CAppException classes for creating NCBI applications.
Defines command line argument related classes.
Defines unified interface to application:
static SLJIT_INLINE sljit_ins l(sljit_gpr r, sljit_s32 d, sljit_gpr x, sljit_gpr b)
AlignCompartment * m_matep
void RemoveLeftIndels(int gapopen1, int gapextend1, int gapopen2, int gapextend2)
void RemoveRightIndels(int gapopen1, int gapextend1, int gapopen2, int gapextend2)
static Uint4 letter(char c)
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4