2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module folder
is aliced
;
34 // ////////////////////////////////////////////////////////////////////////// //
35 public class UpdatingFolderEvent
{ string folderPath
; this (string afolderPath
) { folderPath
= afolderPath
; } }
36 public class UpdatingFolderCompleteEvent
{ string folderPath
; this (string afolderPath
) { folderPath
= afolderPath
; } }
37 public class UpdatingCompleteEvent
{}
40 // ////////////////////////////////////////////////////////////////////////// //
41 // mail folder (one that contains mail base)
42 public final class Folder
{
43 private import core
.sync
.rwmutex
;
46 ReadWriteMutex rwlock
; // "writer" will modify mList; other modifications are done in "reader"
49 // add various cached things here
50 uint[] mList
; // visual list
51 uint mCuridx
; // current message index (in list)
52 uint mMsgtop
; // top visible message (index in list)
57 uint lastHiFromsUpdateCounter
= uint.max
;
58 Highlighter
[] ourHiFroms
;
62 this (const(char)[] afolderpath
) {
63 import std
.path
: buildPath
;
64 if (afolderpath
.length
== 0) assert(0, "wtf?!"); // this is a clear bug
65 mFolderPath
= afolderpath
.idup
;
66 mbase
= new ArticleBase(buildPath(mailRootDir
, mFolderPath
));
68 rwlock
= new ReadWriteMutex(ReadWriteMutex
.Policy
.PREFER_WRITERS
);
69 mHideOldThreads
= false;
73 @property bool needRebuild () nothrow @trusted @nogc { synchronized(this) return mNeedRebuild
; }
74 void markForRebuild () nothrow @trusted @nogc { synchronized(this) mNeedRebuild
= true; }
76 // relative to mail root dir
77 @property string
folderPath () const pure nothrow @safe @nogc { return mFolderPath
; }
79 @property bool hideOldThreads () const pure nothrow @safe @nogc { return mHideOldThreads
; }
80 @property void hideOldThreads (bool v
) {
81 bool doRebuild
= false;
82 synchronized(rwlock
.reader
) {
83 if (mHideOldThreads
!= v
) {
84 conwriteln("NOTICE: '", mFolderPath
, "': hideOldThreads changed to '", v
, "'");
89 if (doRebuild
) buildVisibleList();
92 @property uint curidx () nothrow @safe @nogc { synchronized(rwlock
.reader
) return mCuridx
; }
93 @property void curidx (uint v
) nothrow { synchronized(rwlock
.reader
) curidxNL
= v
; }
95 @property void curidxNL (uint v
) nothrow {
96 if (mList
.length
== 0) return;
97 if (v
> mList
.length
) v
= cast(uint)(mList
.length
-1);
99 if (mCuridx
< mList
.length
) releaseContentNoLock();
101 makeCurrentVisibleNoLock();
105 @property uint msgtop () nothrow @safe @nogc { synchronized(rwlock
.reader
) return mMsgtop
; }
107 @property uint aliveCount () nothrow @safe @nogc { synchronized(rwlock
.reader
) return mbase
.aliveCount
; }
108 @property uint unreadCount () nothrow @safe @nogc { synchronized(rwlock
.reader
) return mbase
.unreadCount
; }
110 @property uint maxNntpIndex () nothrow @safe @nogc { synchronized(rwlock
.reader
) return mbase
.maxNntpIndex
; }
112 @property bool curidxValid () nothrow @safe @nogc { synchronized(rwlock
.reader
) return (mCuridx
< mList
.length
); }
114 // get article index from list element index; not thread-safe
115 //@property uint artidx (int listidx) const pure nothrow @safe @nogc { return (listidx >= 0 && listidx < mList.length ? mList[listidx] : uint.max); }
117 // get article by *visible* index; not thread-safe
118 Article
listartNL (usize idx
) nothrow @trusted @nogc { return (idx
< mList
.length ? mbase
[mList
.ptr
[idx
]] : null); }
120 @property bool empty () nothrow @safe @nogc { synchronized(rwlock
.reader
) return (mList
.length
== 0); }
121 @property uint length () nothrow @safe @nogc { synchronized(rwlock
.reader
) return cast(uint)mList
.length
; }
123 // rebuild active highlighters index if necessary; not thread-safe
124 private void updateActiveHighlights () nothrow {
125 if (lastHiFromsUpdateCounter
!= hitwlistUpdateCounter
) {
126 lastHiFromsUpdateCounter
= hitwlistUpdateCounter
;
127 if (ourHiFroms
.length
) { ourHiFroms
.length
= 0; ourHiFroms
.assumeSafeAppend
; }
128 forEachHitTwit((hi
) {
129 if (hi
.isOurFolder(this)) ourHiFroms
~= hi
;
135 // not thread-safe (should be called only from `withBaseXXX()`
136 bool isHighlightedNL (usize idx
) nothrow @trusted {
137 if (idx
>= mList
.length
) return false;
138 updateActiveHighlights();
139 if (ourHiFroms
.length
) {
140 // check highlighters
141 uint aidx
= mList
.ptr
[idx
];
142 auto art
= mbase
[aidx
];
143 foreach (Highlighter hi
; ourHiFroms
) {
144 if (hi
.hiCheck(this, art
, aidx
, mbase
)) return true;
150 // not thread-safe (should be called only from `withBaseXXX()`
151 // return non-null to twid (may be empty string!)
152 string
isTwittedNL (usize idx
) nothrow @trusted {
153 if (idx
>= mList
.length
) return null;
154 return isTwittedByArtIndexNL(mList
.ptr
[idx
]);
157 string
isTwittedByArtIndexNL (usize aidx
) nothrow @trusted {
158 if (aidx
== 0 || aidx
>= mbase
.length
) return null;
159 updateActiveHighlights();
160 if (ourHiFroms
.length
) {
161 // check highlighters
162 auto art
= mbase
[aidx
];
164 foreach (Highlighter hi
; ourHiFroms
) {
165 if (!art
.valid
) break;
166 auto tw
= hi
.twitCheck(this, art
, aidx
, mbase
);
168 if (tw
.length
) return tw
;
177 // thread-safe; can return `null`
178 // idx is index in *VISIBLE* list, *not* article index in base
179 Account
findAccountToPost (uint idx
=0) nothrow {
180 synchronized(rwlock
.reader
) {
181 auto acc
= accFindNntpByFolder(this);
182 if (acc
!is null) return acc
;
183 acc
= accFindByFolder(this);
184 if (acc
!is null) return acc
;
185 uint aidx
= (idx
< mList
.length ? mList
.ptr
[idx
] : 0);
187 auto art
= mbase
[aidx
];
188 if (art
is null) break;
189 acc
= accFindByMail(art
.frommail
);
190 if (acc
!is null) return acc
;
198 void gotoMsgId (const(char)[] msgid
) {
199 if (msgid
.length
== 0) return;
200 synchronized(rwlock
.reader
) {
201 foreach (immutable lidx
, uint aidx
; mList
) {
202 if (auto art
= mbase
[aidx
]) {
203 if (art
.valid
&& art
.msgid
== msgid
) {
204 if (lidx
!= mCuridx
) {
205 if (mCuridx
< mList
.length
) releaseContentNoLock();
206 mCuridx
= cast(uint)lidx
;
207 makeCurrentVisibleNoLock(true);
216 // return true if unread article found
217 bool moveToNextUnread (bool markAsRead
=false) {
218 uint rdmarkIdx
= uint.max
;
219 synchronized(rwlock
.reader
) {
220 if (mList
.length
== 0 || mbase
.unreadCount
== 0) return false;
222 if (mCuridx
< mList
.length
) {
224 if (!mbase
[mList
[idx
]].unread
) idx
= cast(uint)((idx
+1)%mList
.length
);
226 foreach (immutable _
; 0..mList
.length
) {
227 auto art
= mbase
[mList
[idx
]];
228 if (art
is null ||
!art
.valid || art
.deleted
) continue;
230 if (idx
!= mCuridx
) { releaseContentNoLock(); mCuridx
= idx
; }
231 makeCurrentVisibleNoLock(true);
235 idx
= cast(uint)((idx
+1)%mList
.length
);
238 if (rdmarkIdx
== uint.max
) return false;
239 // release reader, get writer, update flags
240 synchronized(rwlock
.writer
) {
241 if (rdmarkIdx
< mList
.length
) {
242 auto art
= mbase
[mList
[rdmarkIdx
]];
243 if (art
!is null && art
.valid
&& !art
.deleted
&& art
.unread
) {
244 if (mbase
.setRead(mList
[rdmarkIdx
])) mbase
.writeUpdates();
251 void moveToNextThread () {
252 synchronized(rwlock
.reader
) {
253 if (mCuridx
>= mList
.length
) {
254 mCuridx
= (mList
.length ?
cast(uint)(mList
.length
-1) : 0);
256 if (listartNL(mCuridx
).parent
== 0) ++mCuridx
;
257 while (mCuridx
< mList
.length
&& listartNL(mCuridx
).parent
!= 0) ++mCuridx
;
258 if (mCuridx
>= mList
.length
) mCuridx
= cast(uint)(mList
.length
-1);
260 makeCurrentVisibleNoLock(true);
264 // DON'T CALL ANY NON-BASE API!
265 void withBaseReader (scope void delegate (ArticleBase abase
, uint cur
, uint top
, uint[] list
) dg
) {
266 synchronized(rwlock
.reader
) {
267 makeCurrentVisibleNoLock(false);
268 dg(mbase
, mCuridx
, mMsgtop
, mList
);
272 // DON'T CALL ANY NON-BASE API!
273 void withBaseWriter (scope void delegate (ArticleBase abase
) dg
) {
274 synchronized(rwlock
.writer
) dg(mbase
);
277 void releaseContent () nothrow {
278 synchronized(rwlock
.reader
) {
279 releaseContentNoLock();
280 //foreach (immutable aidx; 1..mbase.length) mbase[aidx].clearContent();
284 private void releaseContentNoLock () nothrow {
285 if (mCuridx
< mList
.length
) listartNL(mCuridx
).clearContent();
288 void buildVisibleList () {
291 synchronized(this) mNeedRebuild
= false;
292 synchronized(rwlock
.writer
) {
293 releaseContentNoLock();
295 string curi
= (mCuridx
< mList
.length ?
listartNL(mCuridx
).msgid
: null);
296 string topi
= (mMsgtop
< mList
.length ?
listartNL(mMsgtop
).msgid
: null);
298 mMsgtop
= mMsgtop
.max
;
299 mCuridx
= mCuridx
.max
;
302 mList
.assumeSafeAppend
;
303 mList
.reserve(mbase
.length
);
305 long oldtime
= (Clock
.currTime
-(3*30).days
).toUnixTime
; // don't show threads more than ~3 month old
307 bool veryOldThread (uint idx
) {
309 auto art
= mbase
[idx
];
310 if (art
.time
>= oldtime
) return false;
311 if (!veryOldThread(art
.firstchild
)) return false;
317 // index in base.alist
318 void addArticle (uint idx
) {
320 auto art
= mbase
[idx
];
322 // don't show too old threads
323 if (mHideOldThreads
&& art
.parent
== 0 && art
.time
< oldtime
&& veryOldThread(art
.firstchild
)) {
327 if (mMsgtop
== mMsgtop
.max
&& art
.msgid
== topi
) mMsgtop
= cast(uint)mList
.length
;
328 if (mCuridx
== mCuridx
.max
&& art
.msgid
== curi
) mCuridx
= cast(uint)mList
.length
;
330 addArticle(art
.firstchild
);
335 addArticle(mbase
.first
);
337 if (mMsgtop
== mMsgtop
.max
) mMsgtop
= 0;
338 if (mCuridx
== mCuridx
.max
) {
340 mCuridx
= cast(uint)(mList
.length ? mList
.length
-1 : 0);
346 makeCurrentVisibleNoLock();
348 conwriteln(mFolderPath
, ": ", mbase
.length
, " in base; ", mbase
.aliveCount
," alive; ", mList
.length
, " visible; ", mbase
.unreadCount
, " unread");
352 void makeCurrentVisible (bool center
=false) nothrow @trusted @nogc { synchronized(rwlock
.reader
) makeCurrentVisibleNoLock(center
); }
354 private void makeCurrentVisibleNoLock (bool center
=false) nothrow @trusted @nogc {
355 if (mList
.length
== 0) {
361 if (mCuridx
> mList
.length
) mCuridx
= cast(uint)(mList
.length
-1);
365 int lvis
= fvis
+guiThreadListHeight
/gxTextHeightUtf
-1;
366 if (lvis
< fvis
) lvis
= fvis
;
367 if (mCuridx
< fvis
) {
369 } else if (mCuridx
> lvis ||
(center
&& mCuridx
> lvis
-3)) {
371 if (center
) lvis
/= 2;
372 if (lvis
< 1) lvis
= 1;
373 mMsgtop
= (mCuridx
> lvis ? mCuridx
-lvis
: 0);
378 synchronized(rwlock
.reader
) {
379 if (mList
.length
== 0) return;
380 if (mCuridx
>= mList
.length
) mCuridx
= cast(uint)(mList
.length
-1);
381 if (mCuridx
== 0) return;
382 releaseContentNoLock();
384 makeCurrentVisibleNoLock();
389 synchronized(rwlock
.reader
) {
390 if (mList
.length
== 0) return;
391 if (mCuridx
>= mList
.length
) mCuridx
= cast(uint)(mList
.length
-1);
392 if (mList
.length
-mCuridx
<= 1) return; // at last item
393 releaseContentNoLock();
395 makeCurrentVisibleNoLock();
400 synchronized(rwlock
.reader
) {
401 if (mList
.length
== 0) return;
402 if (mCuridx
>= mList
.length
) mCuridx
= cast(uint)(mList
.length
-1);
403 makeCurrentVisibleNoLock();
404 releaseContentNoLock();
405 if (mCuridx
> mMsgtop
) {
408 int pgsize
= guiThreadListHeight
/gxTextHeightUtf
-1;
409 if (pgsize
< 1) pgsize
= 1;
410 if (mCuridx
> pgsize
) mCuridx
-= pgsize
; else mCuridx
= 0;
412 makeCurrentVisibleNoLock();
416 void movePageDown () {
417 synchronized(rwlock
.reader
) {
418 if (mList
.length
== 0) return;
419 if (mCuridx
>= mList
.length
) mCuridx
= cast(uint)(mList
.length
-1);
420 makeCurrentVisibleNoLock();
421 releaseContentNoLock();
422 int pgsize
= guiThreadListHeight
/gxTextHeightUtf
-1;
423 if (mCuridx
< mMsgtop
+pgsize
) mCuridx
= mMsgtop
+pgsize
; else mCuridx
+= pgsize
;
424 if (mCuridx
>= mList
.length
) mCuridx
= cast(uint)(mList
.length
-1);
425 makeCurrentVisibleNoLock();
430 synchronized(rwlock
.reader
) {
431 if (mList
.length
== 0 || mMsgtop
== 0) return;
432 if (mCuridx
>= mList
.length
) mCuridx
= cast(uint)(mList
.length
-1);
433 makeCurrentVisibleNoLock();
439 synchronized(rwlock
.reader
) {
440 if (mList
.length
== 0) return;
441 if (mCuridx
>= mList
.length
) mCuridx
= cast(uint)(mList
.length
-1);
442 makeCurrentVisibleNoLock();
443 if (mList
.length
-mMsgtop
>= 2) ++mMsgtop
;
447 void moveToFirst () {
448 synchronized(rwlock
.reader
) {
449 if (mList
.length
== 0 || mCuridx
== 0) return;
450 releaseContentNoLock();
452 makeCurrentVisibleNoLock();
457 synchronized(rwlock
.reader
) {
458 if (mList
.length
== 0) return;
459 releaseContentNoLock();
460 mCuridx
= cast(uint)mList
.length
-1;
461 makeCurrentVisibleNoLock();
465 void markAsUnread () {
466 synchronized(rwlock
.writer
) {
467 if (mCuridx
< mList
.length
&& !mbase
.unread(mList
[mCuridx
])) {
468 if (mbase
.setUnread(mList
[mCuridx
])) mbase
.writeUpdates();
474 synchronized(rwlock
.writer
) {
475 if (mCuridx
< mList
.length
&& mbase
.unread(mList
[mCuridx
])) {
476 if (mbase
.setRead(mList
[mCuridx
])) mbase
.writeUpdates();
483 // ////////////////////////////////////////////////////////////////////////// //
484 public __gshared Folder
[] folders
;
485 public __gshared
uint folderTop
;
486 public __gshared
uint mFolderCur
;
489 // ////////////////////////////////////////////////////////////////////////// //
490 public @property uint folderCur () nothrow @trusted @nogc { pragma(inline
, true); return mFolderCur
; }
492 public @property void folderCur (uint idx
) nothrow {
493 if (folders
.length
== 0) return;
494 if (idx
>= folders
.length
) idx
= cast(uint)(folders
.length
-1);
495 if (idx
!= mFolderCur
) {
496 if (mFolderCur
< folders
.length
) folders
[mFolderCur
].releaseContent();
502 // ////////////////////////////////////////////////////////////////////////// //
503 public void saveCurrentFolderAndPosition () {
504 import std
.path
: buildPath
;
506 auto fo
= VFile(buildPath(mailRootDir
, ".uilastpos.rc"), "w");
507 if (auto fld = getActiveFolder
) {
508 fo
.writeln(fld.folderPath
);
509 fld.withBaseReader((abase
, cur
, top
, alist
) {
510 if (cur
< alist
.length
) {
511 fo
.writeln(abase
[alist
[cur
]].msgid
);
517 } catch (Exception e
) {
518 conwriteln("ERROR saving position: ", e
.msg
);
523 public void restoreCurrentFolderAndPosition () {
524 import std
.path
: buildPath
;
526 auto fl
= VFile(buildPath(mailRootDir
, ".uilastpos.rc"));
527 string fpath
= fl
.readln
;
528 string msgid
= fl
.readln
;
529 foreach (immutable fidx
, Folder
fld; folders
) {
530 if (fld.folderPath
== fpath
) {
531 folderCur
= cast(uint)fidx
;
532 folderMakeCurVisible();
533 fld.gotoMsgId(msgid
);
537 } catch (Exception e
) {
542 // ////////////////////////////////////////////////////////////////////////// //
543 public Folder
getSpamFolder () {
544 foreach (Folder
fld; folders
) if (fld.folderPath
== "zz_spam") return fld;
549 public Folder
findFolderByPath (const(char)[] path
) {
550 foreach (Folder
fld; folders
) if (fld.folderPath
== path
) return fld;
555 public Folder
getActiveFolder () { return (folderCur
< folders
.length ? folders
[folderCur
] : null); }
558 public void folderMakeCurVisible () {
559 if (folders
.length
== 0) return;
560 if (folderCur
>= folders
.length
) folderCur
= cast(uint)(folders
.length
-1);
561 if (folderCur
< folderTop
) folderTop
= folderCur
;
562 int hgt
= VBufHeight
/(gxTextHeightUtf
+2)-1;
563 if (hgt
< 1) hgt
= 1;
564 if (folderCur
> folderTop
+hgt
) folderTop
= (folderCur
> hgt ? folderCur
-hgt
: 0);
568 public int folderDepth (usize fidx
) {
569 if (fidx
== 0 || fidx
>= folders
.length
) return 0;
571 foreach (char ch
; folders
[fidx
].folderPath
) if (ch
== '/') ++res
;
576 public string
folderVisName (usize fidx
) {
577 if (fidx
>= folders
.length
) return "<???>";
578 int depth
= folderDepth(fidx
);
579 string res
= folders
[fidx
].folderPath
;
580 foreach (immutable idx
, char ch
; res
) {
582 if (--depth
== 0) return res
[idx
+1..$];
589 private bool isGoodFolderName (const(char)[] s
) {
590 if (s
.length
== 0) return false;
591 if (s
.length
> 0 && s
[$-1] == '/') return false;
592 foreach (char ch
; s
) if (ch
== '\\') return false;
598 public void scanFolders () {
602 if (folders
.length
!= 0) assert(0, "are you nuts?");
604 foreach (DirEntry
de; dirEntries(mailRootDir
, SpanMode
.depth
)) {
605 if (!de.isDir
) continue;
606 if (exists(buildPath(de.name
, ".chiignore"))) continue;
607 if (de.name
.baseName
== "." ||
de.name
.baseName
== "..") continue; // not necessary, but meh
608 string xname
= de.name
[mailRootDir
.length
..$];
609 while (xname
.length
&& xname
[0] == '/') xname
= xname
[1..$];
610 if (!xname
.isGoodFolderName
) continue;
611 folders
~= new Folder(xname
);
614 if (folders
.length
== 0) assert(0, "no active folders found");
615 { import std
.algorithm
: sort
; folders
.sort
!((Folder a
, Folder b
) => a
.folderPath
< b
.folderPath
); }
616 conwriteln(folders
.length
, " folder", (folders
.length
> 1 ?
"s": ""), " found.");