ui uglifying; also, it is now possible to move subwindow with keyboard by pressing...
[chiroptera.git] / folder.d
blobd3773965e43f8137f0c8ed60d628cbe0413cf0c8
1 /* E-Mail Client
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;
19 private:
21 import iv.cmdcon;
22 import iv.strex;
23 import iv.vfs.io;
25 import maildb;
27 import egfx;
28 import egui;
30 import hitwit;
31 import account;
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;
45 private:
46 ReadWriteMutex rwlock; // "writer" will modify mList; other modifications are done in "reader"
47 ArticleBase mbase;
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)
53 string mFolderPath;
54 bool mNeedRebuild;
55 bool mHideOldThreads;
57 uint lastHiFromsUpdateCounter = uint.max;
58 Highlighter[] ourHiFroms;
60 public:
61 // not thread-safe
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));
67 mbase.load();
68 rwlock = new ReadWriteMutex(ReadWriteMutex.Policy.PREFER_WRITERS);
69 mHideOldThreads = false;
70 buildVisibleList();
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, "'");
85 mHideOldThreads = v;
86 doRebuild = true;
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);
98 if (v != mCuridx) {
99 if (mCuridx < mList.length) releaseContentNoLock();
100 mCuridx = v;
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;
130 return false;
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;
147 return false;
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];
163 string twres = null;
164 foreach (Highlighter hi; ourHiFroms) {
165 if (!art.valid) break;
166 auto tw = hi.twitCheck(this, art, aidx, mbase);
167 if (tw !is null) {
168 if (tw.length) return tw;
169 twres = "";
172 return twres;
174 return null;
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);
186 while (aidx) {
187 auto art = mbase[aidx];
188 if (art is null) break;
189 acc = accFindByMail(art.frommail);
190 if (acc !is null) return acc;
191 aidx = art.parent;
194 return null;
197 public:
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);
209 return;
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;
221 uint idx = 0;
222 if (mCuridx < mList.length) {
223 idx = mCuridx;
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;
229 if (art.unread) {
230 if (idx != mCuridx) { releaseContentNoLock(); mCuridx = idx; }
231 makeCurrentVisibleNoLock(true);
232 rdmarkIdx = idx;
233 break;
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();
248 return true;
251 void moveToNextThread () {
252 synchronized(rwlock.reader) {
253 if (mCuridx >= mList.length) {
254 mCuridx = (mList.length ? cast(uint)(mList.length-1) : 0);
255 } else {
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 () {
289 import core.time;
290 import std.datetime;
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;
301 mList.length = 0;
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) {
308 while (idx != 0) {
309 auto art = mbase[idx];
310 if (art.time >= oldtime) return false;
311 if (!veryOldThread(art.firstchild)) return false;
312 idx = art.nextsib;
314 return true;
317 // index in base.alist
318 void addArticle (uint idx) {
319 while (idx != 0) {
320 auto art = mbase[idx];
321 art.clearContent();
322 // don't show too old threads
323 if (mHideOldThreads && art.parent == 0 && art.time < oldtime && veryOldThread(art.firstchild)) {
324 idx = art.nextsib;
325 continue;
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;
329 mList ~= idx;
330 addArticle(art.firstchild);
331 idx = art.nextsib;
335 addArticle(mbase.first);
337 if (mMsgtop == mMsgtop.max) mMsgtop = 0;
338 if (mCuridx == mCuridx.max) {
339 if (curi !is null) {
340 mCuridx = cast(uint)(mList.length ? mList.length-1 : 0);
341 } else {
342 mCuridx = 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) {
356 mMsgtop = 0;
357 mCuridx = 0;
358 return;
361 if (mCuridx > mList.length) mCuridx = cast(uint)(mList.length-1);
363 // below?
364 int fvis = mMsgtop;
365 int lvis = fvis+guiThreadListHeight/gxTextHeightUtf-1;
366 if (lvis < fvis) lvis = fvis;
367 if (mCuridx < fvis) {
368 mMsgtop = mCuridx;
369 } else if (mCuridx > lvis || (center && mCuridx > lvis-3)) {
370 lvis -= fvis;
371 if (center) lvis /= 2;
372 if (lvis < 1) lvis = 1;
373 mMsgtop = (mCuridx > lvis ? mCuridx-lvis : 0);
377 void moveUp () {
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();
383 --mCuridx;
384 makeCurrentVisibleNoLock();
388 void moveDown () {
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();
394 ++mCuridx;
395 makeCurrentVisibleNoLock();
399 void movePageUp () {
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) {
406 mCuridx = mMsgtop;
407 } else {
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();
429 void scrollUp () {
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();
434 --mMsgtop;
438 void scrollDown () {
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();
451 mCuridx = 0;
452 makeCurrentVisibleNoLock();
456 void moveToLast () {
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();
473 void markAsRead () {
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();
497 mFolderCur = idx;
502 // ////////////////////////////////////////////////////////////////////////// //
503 public void saveCurrentFolderAndPosition () {
504 import std.path : buildPath;
505 try {
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);
512 } else {
513 fo.writeln();
517 } catch (Exception e) {
518 conwriteln("ERROR saving position: ", e.msg);
523 public void restoreCurrentFolderAndPosition () {
524 import std.path : buildPath;
525 try {
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);
534 break;
537 } catch (Exception e) {
542 // ////////////////////////////////////////////////////////////////////////// //
543 public Folder getSpamFolder () {
544 foreach (Folder fld; folders) if (fld.folderPath == "zz_spam") return fld;
545 return null;
549 public Folder findFolderByPath (const(char)[] path) {
550 foreach (Folder fld; folders) if (fld.folderPath == path) return fld;
551 return null;
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;
570 int res = 0;
571 foreach (char ch; folders[fidx].folderPath) if (ch == '/') ++res;
572 return 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) {
581 if (ch == '/') {
582 if (--depth == 0) return res[idx+1..$];
585 return res;
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;
593 return true;
597 // not thread-safe
598 public void scanFolders () {
599 import std.file;
600 import std.path;
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.");