Contents:

1. Introduction

This program implements a little utility for Windows users. It installs an icon in the system tray, and keeps track of text snippets from your clipboard (showing the last 5 items that you copied). It also allows you to add small notes.

2. Overview

The code is divided into three main sections: Globals, defining a few global values, Functions, defining the functions (actually, just one), and System port setup, that sets up the system port (used to get Windows messages from the tray icon).

After that, the program consists of calling the set-tray function, that installs the systray icon and attaches a menu to it, and looping forever (see the Forever loop section for the reason we do this).

Overview

Globals
Functions
System port setup

set-tray
forever [
 Forever loop
]

2.1 Forever loop

Every 5 seconds, we check the clipboard. If a new clip is present, we add it to the clipboard history.

We conclude that there is a new clip if all the following conditions are met:

  1. read clipboard:// does not fail and returns a string;
  2. the string is not empty;
  3. the string is not the same as the last clip in the history.

If it is so, we add it to the list (the title for the snippet is generated from the clip text); then if the list is longer than 5 items, we remove the older items (so only 5 are retained). (Note that we are actually only moving the block position; on saving only values from this position onward are saved (since we are not using save/all) and when generating the menu only these are considered.) Then we save the clips and recreate the menu by calling the set-tray function.

Forever loop

wait 5
if all [clip: attempt [read clipboard://] not empty? clip any [empty? clips/2 clip <> second last clips/2]] [
 ; add clip to the list (title generated from clip text)
 insert/only tail clips/2 reduce [trim/lines copy/part clip 20 clip]
 ; show only the last 5 items in the history
 clips/2: skip tail clips/2 -5
 ; save and recreate menu
 attempt [save %clips.txt clips]
 set-tray
]

3. Globals

Three words are used globally. clips holds all the text snippets; it is loaded from the %clips.txt file, if it exists; otherwise, it defaults to example clips. keys holds the keys to the clips from the tray menu. editing? is a boolean value indicating if we are in editing mode or not.

The clips block contains two blocks; the first one contains persistent clips (notes that you can add, edit, remove). The second block contains the last 5 snippets from the system clipboard; older clips are removed to make room for new ones whenever you copy some text into the clipboard. Each clip is a block, with the first value being the clip title, and the second value being the clip contents.

The keys block is a list of words; in the tray menu each item is identified by a word; so this block maps that word to the clip block in the clips block.

Globals

clips: any [
 attempt [load %clips.txt]
 [
  [["Persistent clips" "Here you get persistent clips."]]
  [["Your clipboard" {Here you see the last five items from your clipboard.}]]
 ]
]
keys: []
editing?: no

4. Functions

We define only one function for this program: set-tray. This function creates the tray menu, and installs it.

Functions

set-tray: has [menu w] [
 ; fixed menu items
 menu: compose [
  save: "Add note..."
  edit: (either editing? ["Normal mode"] ["Edit mode"])
  quit: "Quit"
  bar
 ]
 Generate menu dialect and keys
 ; add systray icon with the menu
 set-modes system/ports/system compose/deep/only [
  tray: [
   add main [
    help: "Clips"
    menu: (menu)
   ]
  ]
 ]
]

4.1 Generate menu dialect and keys

Generating the menu means adding key and title (e.g. clip1: "Title for clip 1") to the menu block for each clip, and adding the keys to the keys block so that it's possible to get to the clip from the key.

The menu contains the list of persistent clips (or notes), followed by a separation bar, followed by the clipboard history. The first foreach loop adds the notes, while the second adds the clipboard history.

Generate menu dialect and keys

keys: make block! add length? clips/1 length? clips/2
foreach pclip first clips [
 ; make key word, clip1 for first clip, clip2 for second and so on
 w: to word! join "clip" index? tail keys
 append keys w
 ; add key and title to the menu block (e.g. clip1: "Title for clip 1")
 insert insert tail menu to set-word! w pclip/1
]
append menu 'bar
foreach clip second clips [
 w: to word! join "clip" index? tail keys
 append keys w
 insert insert tail menu to set-word! w clip/1
]

5. System port setup

We need to set up an awake function for the system port, and add the port to the system wait list. The awake function is called whenever there is a message on the system port; we get the message and check it is from the tray icon; if so, we handle it.

The awake function returns false so that wait continues waiting (otherwise wait would return to the caller).

System port setup

system/ports/system/awake: func [port /local msg] [
 ; just in case we have more than one message queued
 while [msg: pick port 1] [
  ; we only handle messages from our systray menu
  if find/match msg [tray main menu] [
   Tray message handling
  ]
 ]
 ; continue waiting
 false
]
; put the sysport in the wait list
insert tail system/ports/wait-list system/ports/system

5.1 Tray message handling

The tray message will be something like [tray main menu key], where the last element is a word indicating the menu that has been chosen (e.g. save or edit or clip1 and so on). We have checked already that the block starts with [tray main menu], so we just consider the last word and pass it to a switch.

If the word is not one of save, edit or quit, then it is the name of a text clip.

Tray message handling

; name of the picked menu item
msg: last msg
switch/default msg [
 ; user choose "Add note..."
 save [
  Handle "Add note..." menu choice
 ]
 ; user choose to switch to or out of edit mode
 edit [
  Handle edit mode switching
 ]
 ; user choose "Quit"
 quit [
  Handle "Quit" menu choice
 ]
] [
 Handle selection of a clip
]

5.1.1 Handle "Add note..." menu choice

This code is evaluated when the user selects the "Add note..." menu item. It shows a window to let the user set the note title and the note text. The note text defaults to the contents of the clipboards (if any). The note is added to the first block in the clips block; then this block is saved to the %clips.txt file to make the changes permanent, and the menu is recreated (by calling set-tray).

Handle "Add note..." menu choice

; let user set note title and text
inform layout [
 across text 70 right "Title:" title: field return
 text 70 right "Note:" note: area any [attempt [read clipboard://] ""] return
 pad 78 btn-enter "Add" [hide-popup]
]
; add the note to the list of persistent clips
insert/only tail clips/1 reduce [title/text note/text]
; save changes to file
attempt [save %clips.txt clips]
; recreate tray menu
set-tray

5.1.2 Handle edit mode switching

This code is called when the user selects the "Edit mode" or "Normal mode" (depending on the current status of the editing? flag) menu item. The mode is toggled (by toggling the value of editing? ) and the menu is recreated (by calling set-tray ).

Note that in this case only the second item in the menu changes, so calling set-tray to regenerate the whole menu is overkill. We should probably cache the latest generated menu, then in this case change only the second item and reinstall it. This change is left to the reader as an exercise.

Handle edit mode switching

editing?: not editing?
set-tray

5.1.3 Handle "Quit" menu choice

This code is called when the user selects the "Quit" menu item. The tray icon is removed and the program quits.

Handle "Quit" menu choice

set-modes port [tray: [remove main]]
quit

5.1.4 Handle selection of a clip

This code is called when the user has selected a clip from the menu (either a note or an item from the clipboard history). Remember that at this point the msg word holds the key to the clip (i.e. clip1, 'clip2 and so on). So, we check for it in the keys block; if it's not found, we ignore it (should never happen); otherwise, we get its index in the keys block (so that we can then pick the clip block from the clips block).

If we are in editing mode, we let the user edit the clip (see the Edit a clip section). Otherwise, we copy the content of the selected clip to the system clipboard (so that the user can paste it somewhere).

Note that the clips are divided into two subblocks inside clips, while the keys block is flat. However, the mapping from keys to clips is trivial. The positions for the keys in the keys block are, as usual, [1 2 3 ... (n+m)] where n is the length of the first block in clips (the notes block) and m is the length of the second (the clipboard history, this will actually be 5 or less - most of the time 5). This means that the positions of the clips are [[1 2 ... n] [1 2 ... m]] ; so we can directly map the first n keys to the first block: if msg is between 1 and n, the clip is taken from the first block. Otherwise, if msg is between n + 1 and n + m , we need to map it to the range 1 ... m (by subtracting n), so we can pick the clip from the second block.

Handle selection of a clip

; one of the clips has been selected?
if msg: find keys msg [
 msg: index? msg
 either editing? [
  Edit a clip
 ] [
  msg: any [pick clips/1 msg pick clips/2 msg - length? clips/1]
  ; put the selected text in the clipboard
  write clipboard:// msg/2
 ]
]

5.1.4.1 Edit a clip

When we are in editing mode and the user selects a clip from the first list (the notes), we want to show a window to let the user edit or delete it. If msg is greater than the length of clips/1 the user has selected a clip from the clipboard history, and we just ignore this. Otherwise (|at clips/1 msg| is not at the tail) we go on with the editing.

The field and the area's text are set directly to the strings in the clip block, so they will edit them directly and no action is necessary to sync between the contents of the user interface and the contents of the clip block.

If the Delete button is clicked, the clip block is removed from the list of the notes.

After either clicking Delete or Ok, the clips block is saved to the %clips.txt file to make the changes permanent, and the menu is recreated (as usual, by calling set-tray).

Edit a clip

msg: at clips/1 msg
if not tail? msg [
 ; let the user edit or delete clip
 inform layout [
  across text 70 right "Title:" title: field msg/1/1 return
  text 70 right "Note:" note: area msg/1/2 return
  pad 78 btn red "Delete" [remove msg hide-popup] btn-enter "Ok" [hide-popup]
 ]
 ; save changes and recreate menu
 attempt [save %clips.txt clips]
 set-tray
]